@tickernelz/paperclip-pro-adapter-omp-local 2026.925.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/LICENSE +22 -0
- package/dist/cli/format-event.d.ts +2 -0
- package/dist/cli/format-event.d.ts.map +1 -0
- package/dist/cli/format-event.js +56 -0
- package/dist/cli/format-event.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/metadata.d.ts +9 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +44 -0
- package/dist/metadata.js.map +1 -0
- package/dist/server/config.d.ts +22 -0
- package/dist/server/config.d.ts.map +1 -0
- package/dist/server/config.js +661 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/execute.cwd.test.d.ts +2 -0
- package/dist/server/execute.cwd.test.d.ts.map +1 -0
- package/dist/server/execute.cwd.test.js +88 -0
- package/dist/server/execute.cwd.test.js.map +1 -0
- package/dist/server/execute.d.ts +8 -0
- package/dist/server/execute.d.ts.map +1 -0
- package/dist/server/execute.js +812 -0
- package/dist/server/execute.js.map +1 -0
- package/dist/server/execute.toggles.test.d.ts +2 -0
- package/dist/server/execute.toggles.test.d.ts.map +1 -0
- package/dist/server/execute.toggles.test.js +147 -0
- package/dist/server/execute.toggles.test.js.map +1 -0
- package/dist/server/failure.d.ts +17 -0
- package/dist/server/failure.d.ts.map +1 -0
- package/dist/server/failure.js +74 -0
- package/dist/server/failure.js.map +1 -0
- package/dist/server/index.d.ts +6 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +97 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/models.d.ts +19 -0
- package/dist/server/models.d.ts.map +1 -0
- package/dist/server/models.js +208 -0
- package/dist/server/models.js.map +1 -0
- package/dist/server/parse.d.ts +36 -0
- package/dist/server/parse.d.ts.map +1 -0
- package/dist/server/parse.js +378 -0
- package/dist/server/parse.js.map +1 -0
- package/dist/server/parse.test.d.ts +2 -0
- package/dist/server/parse.test.d.ts.map +1 -0
- package/dist/server/parse.test.js +29 -0
- package/dist/server/parse.test.js.map +1 -0
- package/dist/server/profile.d.ts +7 -0
- package/dist/server/profile.d.ts.map +1 -0
- package/dist/server/profile.js +40 -0
- package/dist/server/profile.js.map +1 -0
- package/dist/server/progress.d.ts +16 -0
- package/dist/server/progress.d.ts.map +1 -0
- package/dist/server/progress.js +226 -0
- package/dist/server/progress.js.map +1 -0
- package/dist/server/quota.d.ts +4 -0
- package/dist/server/quota.d.ts.map +1 -0
- package/dist/server/quota.js +129 -0
- package/dist/server/quota.js.map +1 -0
- package/dist/server/redaction-repair.d.ts +5 -0
- package/dist/server/redaction-repair.d.ts.map +1 -0
- package/dist/server/redaction-repair.js +122 -0
- package/dist/server/redaction-repair.js.map +1 -0
- package/dist/server/settings-overlay.d.ts +19 -0
- package/dist/server/settings-overlay.d.ts.map +1 -0
- package/dist/server/settings-overlay.js +60 -0
- package/dist/server/settings-overlay.js.map +1 -0
- package/dist/server/skills.d.ts +5 -0
- package/dist/server/skills.d.ts.map +1 -0
- package/dist/server/skills.js +110 -0
- package/dist/server/skills.js.map +1 -0
- package/dist/server/test.d.ts +3 -0
- package/dist/server/test.d.ts.map +1 -0
- package/dist/server/test.js +435 -0
- package/dist/server/test.js.map +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +2 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/parse-stdout.d.ts +4 -0
- package/dist/ui/parse-stdout.d.ts.map +1 -0
- package/dist/ui/parse-stdout.js +480 -0
- package/dist/ui/parse-stdout.js.map +1 -0
- package/dist/ui/parse-stdout.test.d.ts +2 -0
- package/dist/ui/parse-stdout.test.d.ts.map +1 -0
- package/dist/ui/parse-stdout.test.js +69 -0
- package/dist/ui/parse-stdout.test.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { ensurePathInEnv, runChildProcess, } from "@tickernelz/paperclip-pro-adapter-utils/server-utils";
|
|
5
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
6
|
+
import { listOmpModels } from "./models.js";
|
|
7
|
+
import { normalizeOmpProfile, resolveOmpProfile } from "./profile.js";
|
|
8
|
+
const CONFIG_PROBE_TIMEOUT_SEC = 15;
|
|
9
|
+
const CONFIG_BLOCK_ALLOWLIST = {
|
|
10
|
+
defaultThinkingLevel: true,
|
|
11
|
+
disabledExtensions: true,
|
|
12
|
+
disabledProviders: true,
|
|
13
|
+
enabledModels: true,
|
|
14
|
+
extensions: true,
|
|
15
|
+
modelProviderOrder: true,
|
|
16
|
+
modelRoles: true,
|
|
17
|
+
modelTags: true,
|
|
18
|
+
providers: true,
|
|
19
|
+
retry: true,
|
|
20
|
+
skills: true,
|
|
21
|
+
tools: true,
|
|
22
|
+
};
|
|
23
|
+
const MODEL_BLOCK_ALLOWLIST = { modelOverrides: true, providers: true };
|
|
24
|
+
const YAML_FILENAMES = ["config.yml", "config.yaml", "models.yml", "models.yaml"];
|
|
25
|
+
function objectValue(value) {
|
|
26
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
27
|
+
? value
|
|
28
|
+
: {};
|
|
29
|
+
}
|
|
30
|
+
function nonEmptyString(value) {
|
|
31
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
32
|
+
}
|
|
33
|
+
function firstString(...values) {
|
|
34
|
+
for (const value of values) {
|
|
35
|
+
const result = nonEmptyString(value);
|
|
36
|
+
if (result)
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function stringEnv(value) {
|
|
42
|
+
const env = {};
|
|
43
|
+
for (const [key, item] of Object.entries(objectValue(value))) {
|
|
44
|
+
if (typeof item === "string")
|
|
45
|
+
env[key] = item;
|
|
46
|
+
}
|
|
47
|
+
return env;
|
|
48
|
+
}
|
|
49
|
+
function configRoot(env = {}) {
|
|
50
|
+
const configured = firstString(env.PI_CONFIG_DIR, process.env.PI_CONFIG_DIR) ?? ".omp";
|
|
51
|
+
if (configured === "~")
|
|
52
|
+
return os.homedir();
|
|
53
|
+
if (configured.startsWith("~/") || configured.startsWith("~\\")) {
|
|
54
|
+
return path.resolve(os.homedir(), configured.slice(2));
|
|
55
|
+
}
|
|
56
|
+
return path.resolve(os.homedir(), configured);
|
|
57
|
+
}
|
|
58
|
+
function profileAgentDir(profile, env = {}) {
|
|
59
|
+
return profile ? path.join(configRoot(env), "profiles", profile, "agent") : null;
|
|
60
|
+
}
|
|
61
|
+
function runtimeSelection(config) {
|
|
62
|
+
const record = objectValue(config);
|
|
63
|
+
const configuredEnv = stringEnv(record.env);
|
|
64
|
+
const profileSelection = resolveOmpProfile(record);
|
|
65
|
+
const profile = profileSelection.profile;
|
|
66
|
+
const configuredAgentDir = firstString(record.agentDir, configuredEnv.PI_CODING_AGENT_DIR, process.env.PAPERCLIP_OMP_AGENT_DIR, process.env.PI_CODING_AGENT_DIR);
|
|
67
|
+
const root = configRoot(configuredEnv);
|
|
68
|
+
const derivedProfileDir = profileAgentDir(profile, configuredEnv);
|
|
69
|
+
return {
|
|
70
|
+
profile,
|
|
71
|
+
profileSpecified: profileSelection.specified,
|
|
72
|
+
agentDir: profile ? null : configuredAgentDir,
|
|
73
|
+
sourceAgentDir: derivedProfileDir ?? configuredAgentDir ?? path.join(root, "agent"),
|
|
74
|
+
env: configuredEnv,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function resolveOmpCommand(config) {
|
|
78
|
+
return firstString(objectValue(config).command, process.env.PAPERCLIP_OMP_COMMAND) ?? "omp";
|
|
79
|
+
}
|
|
80
|
+
function envPathList(value) {
|
|
81
|
+
const text = nonEmptyString(value);
|
|
82
|
+
if (!text)
|
|
83
|
+
return [];
|
|
84
|
+
if (text.startsWith("[")) {
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(text);
|
|
87
|
+
if (Array.isArray(parsed)) {
|
|
88
|
+
return parsed.filter((item) => typeof item === "string" && item.trim().length > 0)
|
|
89
|
+
.map((item) => item.trim());
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return text.split(text.includes("\n") ? /\r?\n/ : path.delimiter).map((item) => item.trim()).filter(Boolean);
|
|
96
|
+
}
|
|
97
|
+
export function detectionEnv() {
|
|
98
|
+
const profileSelection = resolveOmpProfile({});
|
|
99
|
+
const agentDir = firstString(process.env.PAPERCLIP_OMP_AGENT_DIR);
|
|
100
|
+
const configFiles = envPathList(process.env.PAPERCLIP_OMP_CONFIG_FILES);
|
|
101
|
+
const env = stringEnv(ensurePathInEnv({ ...process.env }));
|
|
102
|
+
if (profileSelection.profile)
|
|
103
|
+
env.OMP_PROFILE = profileSelection.profile;
|
|
104
|
+
else if (profileSelection.specified) {
|
|
105
|
+
delete env.OMP_PROFILE;
|
|
106
|
+
delete env.PI_PROFILE;
|
|
107
|
+
}
|
|
108
|
+
if (agentDir)
|
|
109
|
+
env.PI_CODING_AGENT_DIR = agentDir;
|
|
110
|
+
if (configFiles.length)
|
|
111
|
+
env.PI_CONFIG_FILES = configFiles.join(path.delimiter);
|
|
112
|
+
return env;
|
|
113
|
+
}
|
|
114
|
+
function jsonObjectFromOutput(stdout) {
|
|
115
|
+
const trimmed = stdout.trim();
|
|
116
|
+
const candidates = [trimmed, ...trimmed.split(/\r?\n/).reverse().map((line) => line.trim())];
|
|
117
|
+
for (const candidate of candidates) {
|
|
118
|
+
if (!candidate.startsWith("{") || !candidate.endsWith("}"))
|
|
119
|
+
continue;
|
|
120
|
+
try {
|
|
121
|
+
const parsed = JSON.parse(candidate);
|
|
122
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
123
|
+
return parsed;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
async function configProbe(command, args, env) {
|
|
132
|
+
return runChildProcess(`omp-config-${Date.now()}-${Math.random().toString(16).slice(2)}`, command, args, {
|
|
133
|
+
cwd: process.cwd(),
|
|
134
|
+
env,
|
|
135
|
+
timeoutSec: CONFIG_PROBE_TIMEOUT_SEC,
|
|
136
|
+
graceSec: 3,
|
|
137
|
+
onLog: async () => { },
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
export async function detectModel() {
|
|
141
|
+
const command = resolveOmpCommand({});
|
|
142
|
+
try {
|
|
143
|
+
const env = detectionEnv();
|
|
144
|
+
const result = await configProbe(command, ["config", "get", "modelRoles", "--json"], env);
|
|
145
|
+
if (result.timedOut || (result.exitCode ?? 1) !== 0)
|
|
146
|
+
return null;
|
|
147
|
+
const payload = jsonObjectFromOutput(result.stdout);
|
|
148
|
+
const roles = objectValue(payload?.value);
|
|
149
|
+
const model = nonEmptyString(roles.default);
|
|
150
|
+
if (!model)
|
|
151
|
+
return null;
|
|
152
|
+
const candidates = [];
|
|
153
|
+
for (const value of Object.values(roles)) {
|
|
154
|
+
const selector = nonEmptyString(value);
|
|
155
|
+
if (selector && !candidates.includes(selector))
|
|
156
|
+
candidates.push(selector);
|
|
157
|
+
}
|
|
158
|
+
if (!candidates.includes(model))
|
|
159
|
+
candidates.unshift(model);
|
|
160
|
+
let source = "OMP effective modelRoles.default";
|
|
161
|
+
try {
|
|
162
|
+
const pathResult = await configProbe(command, ["config", "path"], env);
|
|
163
|
+
const agentDir = !pathResult.timedOut && (pathResult.exitCode ?? 1) === 0
|
|
164
|
+
? firstString(pathResult.stdout.split(/\r?\n/)[0])
|
|
165
|
+
: null;
|
|
166
|
+
if (agentDir)
|
|
167
|
+
source = path.join(agentDir, "config.yml");
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
}
|
|
171
|
+
const slash = model.indexOf("/");
|
|
172
|
+
return {
|
|
173
|
+
model,
|
|
174
|
+
provider: slash > 0 ? model.slice(0, slash) : "",
|
|
175
|
+
source,
|
|
176
|
+
...(candidates.length > 1 ? { candidates } : {}),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async function profileOptions() {
|
|
184
|
+
const names = new Set();
|
|
185
|
+
try {
|
|
186
|
+
const entries = await fs.readdir(path.join(configRoot(), "profiles"), { withFileTypes: true });
|
|
187
|
+
for (const entry of entries) {
|
|
188
|
+
if (!entry.isDirectory())
|
|
189
|
+
continue;
|
|
190
|
+
try {
|
|
191
|
+
const normalized = normalizeOmpProfile(entry.name);
|
|
192
|
+
if (normalized)
|
|
193
|
+
names.add(normalized);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const configured = resolveOmpProfile({}).profile;
|
|
203
|
+
if (configured)
|
|
204
|
+
names.add(configured);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
}
|
|
208
|
+
return [
|
|
209
|
+
{ value: "", label: "Default profile" },
|
|
210
|
+
...[...names].sort().map((name) => ({ value: name, label: name })),
|
|
211
|
+
];
|
|
212
|
+
}
|
|
213
|
+
function modelOptions(models) {
|
|
214
|
+
return models.map((model) => {
|
|
215
|
+
const slash = model.id.indexOf("/");
|
|
216
|
+
return {
|
|
217
|
+
value: model.id,
|
|
218
|
+
label: model.label === model.id ? model.id : `${model.label} (${model.id})`,
|
|
219
|
+
...(slash > 0 ? { group: model.id.slice(0, slash) } : {}),
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
export async function getConfigSchema() {
|
|
224
|
+
const [models, detected, profiles] = await Promise.all([
|
|
225
|
+
listOmpModels(),
|
|
226
|
+
detectModel(),
|
|
227
|
+
profileOptions(),
|
|
228
|
+
]);
|
|
229
|
+
const options = modelOptions(models);
|
|
230
|
+
let defaultProfile = "";
|
|
231
|
+
try {
|
|
232
|
+
defaultProfile = resolveOmpProfile({}).profile ?? "";
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
}
|
|
236
|
+
const roleField = (key, label, hint) => ({
|
|
237
|
+
key,
|
|
238
|
+
label,
|
|
239
|
+
type: "combobox",
|
|
240
|
+
options,
|
|
241
|
+
hint,
|
|
242
|
+
group: "Models",
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
fields: [
|
|
246
|
+
{
|
|
247
|
+
key: "model",
|
|
248
|
+
label: "Model",
|
|
249
|
+
type: "combobox",
|
|
250
|
+
options,
|
|
251
|
+
...(detected ? { default: detected.model } : {}),
|
|
252
|
+
hint: "Exact OMP selector (provider/model). Free text is accepted for custom or extension providers.",
|
|
253
|
+
group: "Models",
|
|
254
|
+
},
|
|
255
|
+
roleField("smolModel", "Small/fast model", "Optional selector passed to OMP --smol for cheap auxiliary work."),
|
|
256
|
+
roleField("slowModel", "Slow/reasoning model", "Optional selector passed to OMP --slow for difficult auxiliary work."),
|
|
257
|
+
roleField("planModel", "Planning model", "Optional selector passed to OMP --plan."),
|
|
258
|
+
{
|
|
259
|
+
key: "thinking",
|
|
260
|
+
label: "OMP thinking override",
|
|
261
|
+
type: "select",
|
|
262
|
+
options: ["auto", "off", "minimal", "low", "medium", "high", "xhigh", "max"].map((value) => ({ value, label: value })),
|
|
263
|
+
hint: "Full OMP thinking level. Overrides the top-level Paperclip effort setting.",
|
|
264
|
+
group: "Models",
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
key: "modelCycle",
|
|
268
|
+
label: "Model cycle allowlist",
|
|
269
|
+
type: "text",
|
|
270
|
+
hint: "Optional comma-separated selectors passed to OMP --models.",
|
|
271
|
+
group: "Models",
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
key: "modelsYaml",
|
|
275
|
+
label: "Isolated models.yml",
|
|
276
|
+
type: "textarea",
|
|
277
|
+
hint: "Optional complete models.yml. It runs in an isolated OMP agent directory; reference secrets by environment variable name instead of embedding values.",
|
|
278
|
+
group: "Models",
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
key: "command",
|
|
282
|
+
label: "OMP command",
|
|
283
|
+
type: "text",
|
|
284
|
+
default: resolveOmpCommand({}),
|
|
285
|
+
hint: "Executable name or absolute path. PAPERCLIP_OMP_COMMAND is the global fallback.",
|
|
286
|
+
group: "Runtime",
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
key: "cwd",
|
|
290
|
+
label: "Fallback working directory",
|
|
291
|
+
type: "text",
|
|
292
|
+
hint: "Used only when Paperclip does not provide an execution workspace.",
|
|
293
|
+
group: "Runtime",
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
key: "profile",
|
|
297
|
+
label: "OMP profile",
|
|
298
|
+
type: "combobox",
|
|
299
|
+
options: profiles,
|
|
300
|
+
default: defaultProfile,
|
|
301
|
+
hint: "Named ~/.omp profile. Profiles override agentDir in OMP.",
|
|
302
|
+
group: "Runtime",
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
key: "agentDir",
|
|
306
|
+
label: "OMP agent directory",
|
|
307
|
+
type: "text",
|
|
308
|
+
default: firstString(process.env.PAPERCLIP_OMP_AGENT_DIR) ?? undefined,
|
|
309
|
+
hint: "Optional PI_CODING_AGENT_DIR override for the default profile.",
|
|
310
|
+
group: "Runtime",
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
key: "timeoutSec",
|
|
314
|
+
label: "Process timeout seconds",
|
|
315
|
+
type: "number",
|
|
316
|
+
default: 43200,
|
|
317
|
+
hint: "Paperclip terminates the OMP process after this many seconds. 43200 = 12 hours, sized for long autonomous runs.",
|
|
318
|
+
group: "Runtime",
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
key: "graceSec",
|
|
322
|
+
label: "Termination grace seconds",
|
|
323
|
+
type: "number",
|
|
324
|
+
default: 20,
|
|
325
|
+
hint: "Seconds between the stop signal and SIGKILL, so OMP can close the session and flush its session file.",
|
|
326
|
+
group: "Runtime",
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
key: "maxTime",
|
|
330
|
+
label: "OMP max time seconds",
|
|
331
|
+
type: "text",
|
|
332
|
+
hint: "Optional in-session deadline passed to OMP --max-time.",
|
|
333
|
+
group: "Runtime",
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
key: "sessionDir",
|
|
337
|
+
label: "Session directory",
|
|
338
|
+
type: "text",
|
|
339
|
+
hint: "Optional OMP session directory override. Paperclip otherwise uses an agent-scoped directory.",
|
|
340
|
+
group: "Runtime",
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
key: "noSession",
|
|
344
|
+
label: "Ephemeral session",
|
|
345
|
+
type: "toggle",
|
|
346
|
+
default: false,
|
|
347
|
+
hint: "Pass --no-session and return no resumable Paperclip session metadata.",
|
|
348
|
+
group: "Runtime",
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
key: "allowHome",
|
|
352
|
+
label: "Allow home as cwd",
|
|
353
|
+
type: "toggle",
|
|
354
|
+
default: false,
|
|
355
|
+
hint: "Pass OMP --allow-home when the resolved workspace is the user's home directory.",
|
|
356
|
+
group: "Runtime",
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
key: "approvalMode",
|
|
360
|
+
label: "Approval mode",
|
|
361
|
+
type: "select",
|
|
362
|
+
default: "yolo",
|
|
363
|
+
options: [
|
|
364
|
+
{ value: "yolo", label: "Auto-approve (yolo)" },
|
|
365
|
+
{ value: "write", label: "Ask before writes" },
|
|
366
|
+
{ value: "always-ask", label: "Always ask" },
|
|
367
|
+
],
|
|
368
|
+
hint: "Headless runs default to yolo; choose a stricter mode when interactive approval is available. Applied even when the field is empty, so the run does not depend on tools.approvalMode in the OMP config.",
|
|
369
|
+
group: "Capabilities",
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
key: "printThoughts",
|
|
373
|
+
label: "Print thinking thoughts",
|
|
374
|
+
type: "toggle",
|
|
375
|
+
default: true,
|
|
376
|
+
hint: "Thinking blocks are streamed into the Paperclip transcript. Turn off to shrink run logs.",
|
|
377
|
+
group: "Capabilities",
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
key: "addDirs",
|
|
381
|
+
label: "Secondary workspaces",
|
|
382
|
+
type: "textarea",
|
|
383
|
+
hint: "One workspace root path per line passed to OMP via --add-dir.",
|
|
384
|
+
group: "Runtime",
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
key: "tools",
|
|
388
|
+
label: "Tool allowlist",
|
|
389
|
+
type: "text",
|
|
390
|
+
hint: "Comma-separated OMP tool names. Empty preserves OMP's complete default toolset.",
|
|
391
|
+
group: "Capabilities",
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
key: "noTools",
|
|
395
|
+
label: "Disable default tools",
|
|
396
|
+
type: "toggle",
|
|
397
|
+
default: false,
|
|
398
|
+
hint: "Pass --no-tools. Combine with a tool allowlist to enable only those tools.",
|
|
399
|
+
group: "Capabilities",
|
|
400
|
+
},
|
|
401
|
+
{ key: "advisor", label: "Enable advisor", type: "toggle", default: false, group: "Capabilities" },
|
|
402
|
+
{ key: "noPrewalk", label: "Disable configured prewalk", type: "toggle", default: false, group: "Capabilities" },
|
|
403
|
+
{
|
|
404
|
+
key: "prewalk",
|
|
405
|
+
label: "Enable prewalk",
|
|
406
|
+
type: "toggle",
|
|
407
|
+
default: false,
|
|
408
|
+
group: "Capabilities",
|
|
409
|
+
meta: { visibleWhen: { key: "noPrewalk", notValues: ["true"] } },
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
key: "prewalkInto",
|
|
413
|
+
label: "Prewalk target model",
|
|
414
|
+
type: "text",
|
|
415
|
+
group: "Capabilities",
|
|
416
|
+
meta: { visibleWhen: { key: "noPrewalk", notValues: ["true"] } },
|
|
417
|
+
},
|
|
418
|
+
{ key: "planYolo", label: "Enable plan yolo", type: "toggle", default: false, group: "Capabilities" },
|
|
419
|
+
{
|
|
420
|
+
key: "planYoloInto",
|
|
421
|
+
label: "Plan yolo target model",
|
|
422
|
+
type: "text",
|
|
423
|
+
group: "Capabilities",
|
|
424
|
+
meta: { visibleWhen: { key: "planYolo", value: "true" } },
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
key: "configFiles",
|
|
428
|
+
label: "Config overlays",
|
|
429
|
+
type: "textarea",
|
|
430
|
+
hint: "One OMP config.yml overlay path per line, in precedence order.",
|
|
431
|
+
group: "Extensions",
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
key: "extensions",
|
|
435
|
+
label: "Extensions",
|
|
436
|
+
type: "textarea",
|
|
437
|
+
hint: "One OMP extension path per line.",
|
|
438
|
+
group: "Extensions",
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
key: "pluginDirs",
|
|
442
|
+
label: "Plugin directories",
|
|
443
|
+
type: "textarea",
|
|
444
|
+
hint: "One OMP --plugin-dir path per line.",
|
|
445
|
+
group: "Extensions",
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
key: "hooks",
|
|
449
|
+
label: "Hooks",
|
|
450
|
+
type: "textarea",
|
|
451
|
+
hint: "One OMP hook/extension path per line.",
|
|
452
|
+
group: "Extensions",
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
key: "skills",
|
|
456
|
+
label: "Skill filter",
|
|
457
|
+
type: "text",
|
|
458
|
+
hint: "Optional comma-separated OMP skill names. Empty keeps normal skill discovery.",
|
|
459
|
+
group: "Extensions",
|
|
460
|
+
},
|
|
461
|
+
{ key: "noExtensions", label: "Disable extension discovery", type: "toggle", default: false, group: "Extensions" },
|
|
462
|
+
{ key: "noSkills", label: "Disable skills", type: "toggle", default: false, group: "Extensions" },
|
|
463
|
+
{ key: "noRules", label: "Disable rules", type: "toggle", default: false, group: "Extensions" },
|
|
464
|
+
{ key: "noLsp", label: "Disable LSP", type: "toggle", default: false, group: "Capabilities" },
|
|
465
|
+
{ key: "noPty", label: "Disable PTY tools", type: "toggle", default: false, group: "Capabilities" },
|
|
466
|
+
{ key: "noTitle", label: "Disable terminal title", type: "toggle", default: true, group: "Capabilities" },
|
|
467
|
+
{
|
|
468
|
+
key: "systemPrompt",
|
|
469
|
+
label: "OMP base system prompt",
|
|
470
|
+
type: "textarea",
|
|
471
|
+
hint: "Optional replacement for OMP's base system prompt. Paperclip's execution contract is still appended.",
|
|
472
|
+
group: "Advanced",
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
key: "extraArgs",
|
|
476
|
+
label: "Extra OMP arguments",
|
|
477
|
+
type: "textarea",
|
|
478
|
+
hint: "One literal argument per line. Arguments are passed directly without shell evaluation.",
|
|
479
|
+
group: "Advanced",
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
key: "instructionsFilePath",
|
|
483
|
+
label: "Instructions bundle path",
|
|
484
|
+
type: "text",
|
|
485
|
+
hint: "Optional Paperclip-managed instructions bundle path.",
|
|
486
|
+
group: "Advanced",
|
|
487
|
+
},
|
|
488
|
+
],
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function parseYamlRecord(content) {
|
|
492
|
+
if (/\t/.test(content) || /(?:^|\s)!(?:!|<)/.test(content)) {
|
|
493
|
+
throw new Error("OMP YAML contains unsupported tags or tab indentation.");
|
|
494
|
+
}
|
|
495
|
+
const parsed = parseYaml(content, { maxAliasCount: 50 });
|
|
496
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
497
|
+
throw new Error("OMP YAML must contain a top-level mapping.");
|
|
498
|
+
}
|
|
499
|
+
return parsed;
|
|
500
|
+
}
|
|
501
|
+
function sensitiveKey(key) {
|
|
502
|
+
const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
503
|
+
if (normalized === "apikey")
|
|
504
|
+
return "apiKey";
|
|
505
|
+
if (/(?:secret|password|credential|cookie|authorization|privatekey|clientsecret|accesskey|headers?)$/.test(normalized) ||
|
|
506
|
+
(normalized.endsWith("token") && normalized !== "maxtokens" && normalized !== "inputtokens" && normalized !== "outputtokens"))
|
|
507
|
+
return "secret";
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
function stringContainsCredential(value) {
|
|
511
|
+
return (value.includes("$(") ||
|
|
512
|
+
/:\/\/[^\s/@]+@/.test(value) ||
|
|
513
|
+
/[?&#](?:api[_-]?key|access[_-]?token|token|secret|password|auth(?:orization)?)(?:=|%3d)/i.test(value));
|
|
514
|
+
}
|
|
515
|
+
function containsCredential(value, env, key = "", seen = new WeakSet()) {
|
|
516
|
+
const kind = sensitiveKey(key);
|
|
517
|
+
if (kind === "apiKey") {
|
|
518
|
+
return typeof value !== "string" ||
|
|
519
|
+
!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value) ||
|
|
520
|
+
!nonEmptyString(env[value]);
|
|
521
|
+
}
|
|
522
|
+
if (kind === "secret")
|
|
523
|
+
return true;
|
|
524
|
+
if (typeof value === "string")
|
|
525
|
+
return stringContainsCredential(value);
|
|
526
|
+
if (value === null || typeof value !== "object")
|
|
527
|
+
return false;
|
|
528
|
+
if (seen.has(value))
|
|
529
|
+
return true;
|
|
530
|
+
seen.add(value);
|
|
531
|
+
if (Array.isArray(value)) {
|
|
532
|
+
return value.some((item) => containsCredential(item, env, "", seen));
|
|
533
|
+
}
|
|
534
|
+
return Object.entries(value).some(([childKey, item]) => containsCredential(item, env, childKey, seen));
|
|
535
|
+
}
|
|
536
|
+
function projectYaml(content, kind, env, options = {}) {
|
|
537
|
+
const parsed = parseYamlRecord(content);
|
|
538
|
+
const allowlist = kind === "config" ? CONFIG_BLOCK_ALLOWLIST : MODEL_BLOCK_ALLOWLIST;
|
|
539
|
+
const projected = Object.create(null);
|
|
540
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
541
|
+
if (!Object.hasOwn(allowlist, key))
|
|
542
|
+
continue;
|
|
543
|
+
if (kind === "config" && options.omitPathSettings && key === "extensions") {
|
|
544
|
+
options.notes?.push("Skipped config extensions during remote materialization; use workspace-relative adapter extensions instead.");
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (key === "providers" && value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
548
|
+
const providers = Object.create(null);
|
|
549
|
+
for (const [provider, providerConfig] of Object.entries(value)) {
|
|
550
|
+
if (containsCredential(providerConfig, env)) {
|
|
551
|
+
options.notes?.push(`Skipped provider ${provider}: credential-bearing configuration cannot be materialized.`);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
providers[provider] = providerConfig;
|
|
555
|
+
}
|
|
556
|
+
if (Object.keys(providers).length > 0)
|
|
557
|
+
projected[key] = providers;
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
if (!containsCredential(value, env, key))
|
|
561
|
+
projected[key] = value;
|
|
562
|
+
}
|
|
563
|
+
return Object.keys(projected).length > 0 ? stringifyYaml(projected, { lineWidth: 0 }) : null;
|
|
564
|
+
}
|
|
565
|
+
function validatedInlineModelsYaml(content, env) {
|
|
566
|
+
const parsed = parseYamlRecord(content);
|
|
567
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
568
|
+
if (!Object.hasOwn(MODEL_BLOCK_ALLOWLIST, key))
|
|
569
|
+
continue;
|
|
570
|
+
if (containsCredential(value, env, key)) {
|
|
571
|
+
throw new Error("modelsYaml contains credential-bearing fields. Bind secrets through config.env and reference the environment variable name from apiKey.");
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const projected = projectYaml(content, "models", env);
|
|
575
|
+
if (!projected) {
|
|
576
|
+
throw new Error("modelsYaml contains no supported modelOverrides or providers configuration.");
|
|
577
|
+
}
|
|
578
|
+
return projected;
|
|
579
|
+
}
|
|
580
|
+
async function copySanitizedYaml(sourceDir, targetDir, filenames, notes, env, omitPathSettings = false) {
|
|
581
|
+
for (const filename of filenames) {
|
|
582
|
+
const source = path.join(sourceDir, filename);
|
|
583
|
+
let content;
|
|
584
|
+
try {
|
|
585
|
+
content = await fs.readFile(source, "utf8");
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
if (error.code === "ENOENT")
|
|
589
|
+
continue;
|
|
590
|
+
notes.push(`Could not read ${filename}; it was not materialized.`);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
const projected = projectYaml(content, filename.startsWith("models.") ? "models" : "config", env, { omitPathSettings, notes });
|
|
594
|
+
if (!projected) {
|
|
595
|
+
notes.push(`Skipped ${filename}: no whitelisted, non-secret configuration could be materialized safely.`);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
await fs.writeFile(path.join(targetDir, filename), projected, { encoding: "utf8", mode: 0o600 });
|
|
599
|
+
notes.push(`Materialized sanitized ${filename} without auth state.`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
export async function prepareOmpRuntimeConfig(config, options = {}) {
|
|
603
|
+
const record = objectValue(config);
|
|
604
|
+
const selection = runtimeSelection(record);
|
|
605
|
+
const hasInlineModels = typeof record.modelsYaml === "string" && record.modelsYaml.trim().length > 0;
|
|
606
|
+
const mustMaterialize = hasInlineModels || options.forceMaterialized === true;
|
|
607
|
+
if (!mustMaterialize) {
|
|
608
|
+
return {
|
|
609
|
+
agentDir: selection.agentDir,
|
|
610
|
+
profile: selection.profile,
|
|
611
|
+
profileSpecified: selection.profileSpecified,
|
|
612
|
+
materialized: false,
|
|
613
|
+
notes: selection.profile && firstString(record.agentDir)
|
|
614
|
+
? ["OMP profile selection takes precedence over agentDir."]
|
|
615
|
+
: [],
|
|
616
|
+
cleanup: async () => { },
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
const notes = [];
|
|
620
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-omp-agent-"));
|
|
621
|
+
await fs.chmod(tempDir, 0o700);
|
|
622
|
+
let cleaned = false;
|
|
623
|
+
const materializationEnv = { ...stringEnv(process.env), ...selection.env };
|
|
624
|
+
try {
|
|
625
|
+
if (selection.sourceAgentDir) {
|
|
626
|
+
const files = hasInlineModels
|
|
627
|
+
? ["config.yml", "config.yaml"]
|
|
628
|
+
: YAML_FILENAMES;
|
|
629
|
+
await copySanitizedYaml(selection.sourceAgentDir, tempDir, files, notes, materializationEnv, options.forceMaterialized === true);
|
|
630
|
+
}
|
|
631
|
+
if (hasInlineModels) {
|
|
632
|
+
const inlineModels = validatedInlineModelsYaml(record.modelsYaml, materializationEnv);
|
|
633
|
+
await fs.writeFile(path.join(tempDir, "models.yml"), inlineModels, {
|
|
634
|
+
encoding: "utf8",
|
|
635
|
+
mode: 0o600,
|
|
636
|
+
});
|
|
637
|
+
notes.push("Using a sanitized inline models.yml; credentials must remain in environment bindings.");
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
notes.push("Using a materialized OMP agent directory; agent.db and secret-bearing configuration were not copied.");
|
|
641
|
+
}
|
|
642
|
+
return {
|
|
643
|
+
agentDir: tempDir,
|
|
644
|
+
profile: selection.profile,
|
|
645
|
+
profileSpecified: selection.profileSpecified,
|
|
646
|
+
materialized: true,
|
|
647
|
+
notes,
|
|
648
|
+
cleanup: async () => {
|
|
649
|
+
if (cleaned)
|
|
650
|
+
return;
|
|
651
|
+
cleaned = true;
|
|
652
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
653
|
+
},
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
658
|
+
throw error;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
//# sourceMappingURL=config.js.map
|