@hiveku-apps/sync 0.3.5 → 0.3.6
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 +46 -3
- package/bin/hiveku-sync.mjs +26 -0
- package/package.json +1 -1
- package/src/codex-prompt-tools.json +252 -0
- package/src/codex.mjs +238 -28
- package/src/toml.mjs +666 -0
package/src/codex.mjs
CHANGED
|
@@ -17,8 +17,11 @@
|
|
|
17
17
|
* the OLYMPUS_API_KEY env var). git-ignored.
|
|
18
18
|
* skill/command knowledge → .agents/skills/<name>/SKILL.md (repo-scoped; Codex
|
|
19
19
|
* prompts are global-only + deprecated)
|
|
20
|
-
* permissions →
|
|
21
|
-
*
|
|
20
|
+
* permissions → the hiveku server's per-tool approvals: a FIXED
|
|
21
|
+
* list of high-risk tools asks first
|
|
22
|
+
* (CODEX_PROMPT_TOOLS below), every other tool runs
|
|
23
|
+
* without asking. The safe-work rules are stated
|
|
24
|
+
* as instructions in AGENTS.md, for every tool.
|
|
22
25
|
*
|
|
23
26
|
* This complements the published Hiveku Codex plugin
|
|
24
27
|
* (github.com/locus2012/hiveku-codex-plugin): the plugin ships the generic
|
|
@@ -29,27 +32,94 @@
|
|
|
29
32
|
*/
|
|
30
33
|
|
|
31
34
|
import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
|
|
32
|
-
import { existsSync } from 'node:fs';
|
|
35
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
33
36
|
import { homedir } from 'node:os';
|
|
34
37
|
import { join, resolve, dirname } from 'node:path';
|
|
35
38
|
|
|
39
|
+
import { formatKey, readTomlDeclarations } from './toml.mjs';
|
|
36
40
|
import { loadDotenv, safeSlug } from './util.mjs';
|
|
37
41
|
|
|
42
|
+
/**
|
|
43
|
+
* The FIXED list of high-risk Hiveku tools that Codex asks about before
|
|
44
|
+
* running. It is the Hiveku Codex plugin's own prompt list, copied name for
|
|
45
|
+
* name (codex-prompt-tools.json says which release; 246 names at plugin
|
|
46
|
+
* 0.1.16). It includes hiveku_batch, because Codex asks by the name of the tool
|
|
47
|
+
* it calls and a batch carries other tools.
|
|
48
|
+
*
|
|
49
|
+
* It is a list of names, not a category. Every tool not on it runs without
|
|
50
|
+
* asking under the "approve" default, and that includes many deletes and sends
|
|
51
|
+
* (at MCP d4a73ba6d, for example crm_delete_contact, content_delete,
|
|
52
|
+
* assets_delete, survey_send and accounting_invoice_send). That is why
|
|
53
|
+
* AGENTS.md keeps the safe-work rules as instructions for every tool.
|
|
54
|
+
*
|
|
55
|
+
* A listed tool does not always ask either. Codex checks the run mode before
|
|
56
|
+
* it reads a tool's approval_mode (codex-mcp mcp_permission_prompt_is_auto_approved):
|
|
57
|
+
* - approval policy "never" (what `codex exec` sets) with a read-only or
|
|
58
|
+
* workspace-write sandbox: nobody can answer, so a listed call is
|
|
59
|
+
* cancelled and the rest run;
|
|
60
|
+
* - approval policy "never" with no sandbox or full disk write
|
|
61
|
+
* (--dangerously-bypass-approvals-and-sandbox, i.e. --yolo, or
|
|
62
|
+
* danger-full-access in `codex exec`): every prompt is skipped, these
|
|
63
|
+
* included, and every Hiveku tool runs unattended.
|
|
64
|
+
*
|
|
65
|
+
* Why this folder has to write it: the folder's .codex/config.toml defines
|
|
66
|
+
* [mcp_servers.hiveku]. Codex 0.131 then drops the plugin's server of the same
|
|
67
|
+
* name (core/src/config/mod.rs: configured servers win, plugin servers are
|
|
68
|
+
* only `or_insert`ed) and takes each tool's approval mode from the config
|
|
69
|
+
* layers alone (core/src/mcp_tool_call.rs custom_mcp_tool_approval_mode:
|
|
70
|
+
* tools.<tool>.approval_mode, else default_tools_approval_mode; the plugin is
|
|
71
|
+
* read only when no config layer names the server). With only
|
|
72
|
+
* default_tools_approval_mode = "approve" here, every Hiveku tool ran without
|
|
73
|
+
* a prompt, with or without the plugin installed.
|
|
74
|
+
*
|
|
75
|
+
* Adding another prompt list (the unmerged Versions Wave 2 branch has
|
|
76
|
+
* RESTORE_TOOLS, for example): put its names into this one Set, so each tool
|
|
77
|
+
* still gets exactly one table. A second block of tables would define some
|
|
78
|
+
* tables twice (four of Wave 2's five names are already here), and that makes
|
|
79
|
+
* the whole config.toml invalid TOML.
|
|
80
|
+
*
|
|
81
|
+
* The user's own table wins: a listed tool that already has its own
|
|
82
|
+
* [mcp_servers.hiveku.tools.<tool>] table outside the managed block gets no
|
|
83
|
+
* table from us (userToolTables below), and writeCodexScaffold writes nothing
|
|
84
|
+
* when the block would still declare a table the file declares (blockConflict).
|
|
85
|
+
*/
|
|
86
|
+
export const CODEX_PROMPT_TOOLS = [
|
|
87
|
+
...new Set(JSON.parse(readFileSync(new URL('./codex-prompt-tools.json', import.meta.url), 'utf8')).tools),
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
/** A TOML bare key when the name allows it, a quoted key otherwise. */
|
|
91
|
+
function tomlKey(name) {
|
|
92
|
+
return /^[A-Za-z0-9_-]+$/.test(name) ? name : JSON.stringify(name);
|
|
93
|
+
}
|
|
94
|
+
|
|
38
95
|
const AGENTS_BEGIN = '<!-- hiveku:begin -->';
|
|
39
96
|
const AGENTS_END = '<!-- hiveku:end -->';
|
|
40
97
|
const TOML_BEGIN = '# hiveku:begin (managed by hiveku-sync — do not edit inside)';
|
|
41
98
|
const TOML_END = '# hiveku:end';
|
|
42
99
|
const SKILL_STAMP = '<!-- generated-by: hiveku-sync (mirrored from account knowledge) -->';
|
|
43
100
|
|
|
44
|
-
/**
|
|
45
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Replace (or append) the marked region inside existing content. Also says
|
|
103
|
+
* where: `start` is the region's offset in `text`, and `replaced` the old
|
|
104
|
+
* region it replaced (null when it was appended).
|
|
105
|
+
*/
|
|
106
|
+
function placeMarkedRegion(existing, begin, end, region) {
|
|
46
107
|
const b = existing.indexOf(begin);
|
|
47
108
|
const e = existing.indexOf(end);
|
|
48
109
|
if (b !== -1 && e !== -1 && e > b) {
|
|
49
|
-
return
|
|
110
|
+
return {
|
|
111
|
+
text: existing.slice(0, b) + region + existing.slice(e + end.length),
|
|
112
|
+
start: b,
|
|
113
|
+
replaced: existing.slice(b, e + end.length),
|
|
114
|
+
};
|
|
50
115
|
}
|
|
51
116
|
const sep = existing.length && !existing.endsWith('\n\n') ? (existing.endsWith('\n') ? '\n' : '\n\n') : '';
|
|
52
|
-
return existing + sep + region + '\n';
|
|
117
|
+
return { text: existing + sep + region + '\n', start: existing.length + sep.length, replaced: null };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Replace (or append) the marked region inside existing content. */
|
|
121
|
+
function upsertMarkedRegion(existing, begin, end, region) {
|
|
122
|
+
return placeMarkedRegion(existing, begin, end, region).text;
|
|
53
123
|
}
|
|
54
124
|
|
|
55
125
|
/** AGENTS.md — lean on purpose (Codex caps the instruction chain at ~32KiB). */
|
|
@@ -94,13 +164,117 @@ Non-negotiables (also in CLAUDE.md, restated because they are load-bearing):
|
|
|
94
164
|
- Video generation is paid + capped — \`marketing_generate_video\` with \`dry_run: true\` first.
|
|
95
165
|
|
|
96
166
|
Discover tools with \`hiveku_docs_search\` / \`hiveku_docs_get\` and \`hiveku_playbooks_list\` /
|
|
97
|
-
\`hiveku_playbook_get\`. Codex
|
|
98
|
-
|
|
167
|
+
\`hiveku_playbook_get\`. Codex asks the user first only for a fixed list of ${CODEX_PROMPT_TOOLS.length} high-risk Hiveku
|
|
168
|
+
tools (hiveku-sync's \`codex-prompt-tools.json\`, the Hiveku Codex plugin's list, written as per-tool
|
|
169
|
+
prompts in \`.codex/config.toml\`). Every other Hiveku tool, including many deletes and sends, runs
|
|
170
|
+
without asking, and some Codex modes (\`--yolo\`, \`danger-full-access\`) skip even those prompts. So
|
|
171
|
+
never count on a prompt: the rules above are instructions, not enforced permissions — follow them for
|
|
172
|
+
every tool.
|
|
99
173
|
${AGENTS_END}`;
|
|
100
174
|
}
|
|
101
175
|
|
|
176
|
+
/**
|
|
177
|
+
* The listed tools whose table the user's own settings already declare
|
|
178
|
+
* outside the managed block, in list order. Writing ours as well would
|
|
179
|
+
* declare that table twice, which TOML does not allow, so theirs wins.
|
|
180
|
+
*
|
|
181
|
+
* A tool's table is declared by a [mcp_servers.hiveku.tools.<tool>] or
|
|
182
|
+
* [[...]] header, however it is spelled (quoted parts, spaces), or by a key
|
|
183
|
+
* that sets it from a table above it: `<tool> = { ... }` or
|
|
184
|
+
* `<tool>.approval_mode = ...` under [mcp_servers.hiveku.tools]. A header one
|
|
185
|
+
* level deeper ([...tools.<tool>.x]) does not declare it, so it does not count.
|
|
186
|
+
* The file is read as Codex reads it (TOML 1.1 syntax, a byte-order mark at
|
|
187
|
+
* the start); a line that cannot be read is skipped.
|
|
188
|
+
*
|
|
189
|
+
* `withoutApprovalMode`: those whose own table sets no approval_mode, so
|
|
190
|
+
* Codex uses the server default ("approve") for them.
|
|
191
|
+
*/
|
|
192
|
+
function userToolTables(existing) {
|
|
193
|
+
const text = existing.replace(/\r\n/g, '\n');
|
|
194
|
+
const begin = text.indexOf(TOML_BEGIN);
|
|
195
|
+
const end = text.indexOf(TOML_END);
|
|
196
|
+
const blockEnd = begin !== -1 && end > begin ? end + TOML_END.length : -1;
|
|
197
|
+
const found = new Map(); // tool -> { declared, setsMode, notATable }
|
|
198
|
+
for (const statement of readTomlDeclarations(text).statements) {
|
|
199
|
+
if (blockEnd !== -1 && statement.pos >= begin && statement.pos < blockEnd) continue; // ours
|
|
200
|
+
const keyval = statement.type === 'keyval';
|
|
201
|
+
const key = keyval ? [...statement.header, ...statement.key] : statement.key;
|
|
202
|
+
if (key.length < 4 || key[0] !== 'mcp_servers' || key[1] !== 'hiveku' || key[2] !== 'tools') continue;
|
|
203
|
+
const entry = found.get(key[3]) ?? { declared: false, setsMode: false, notATable: false };
|
|
204
|
+
if (keyval ? statement.header.length < 4 : key.length === 4) entry.declared = true;
|
|
205
|
+
if (key[4] === 'approval_mode') entry.setsMode = true;
|
|
206
|
+
if (keyval && key.length === 4) {
|
|
207
|
+
if (statement.value !== 'table') entry.notATable = true;
|
|
208
|
+
else if (statement.tableKeys.includes('approval_mode')) entry.setsMode = true;
|
|
209
|
+
}
|
|
210
|
+
if (statement.type === 'arrayTable' && key.length === 4) entry.notATable = true;
|
|
211
|
+
found.set(key[3], entry);
|
|
212
|
+
}
|
|
213
|
+
const tools = CODEX_PROMPT_TOOLS.filter((tool) => found.get(tool)?.declared);
|
|
214
|
+
const withoutApprovalMode = tools.filter((tool) => !found.get(tool).setsMode && !found.get(tool).notATable);
|
|
215
|
+
return { tools, withoutApprovalMode };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Where the update would declare a table or key twice with one of the two in
|
|
220
|
+
* hiveku-sync's own block: the one way writing the block can break a file.
|
|
221
|
+
* Returns the first such place as { ours, line }, `ours` being the block's
|
|
222
|
+
* statement and `line` the other declaration's line in `existing` (the file
|
|
223
|
+
* as it is now), or null. A problem in the rest of the file is the user's own
|
|
224
|
+
* and not a reason to stop: the block is written as before.
|
|
225
|
+
*
|
|
226
|
+
* `placed` is placeMarkedRegion's result for `region` in `existing`.
|
|
227
|
+
*/
|
|
228
|
+
function blockConflict(existing, placed, region) {
|
|
229
|
+
const normalize = (text) => text.replace(/\r\n/g, '\n');
|
|
230
|
+
const start = normalize(placed.text.slice(0, placed.start)).length;
|
|
231
|
+
const end = start + region.length;
|
|
232
|
+
const inBlock = (statement) => statement !== null && statement.pos >= start && statement.pos < end;
|
|
233
|
+
// Text before the block is as it was; text after it moved by the old block's length less the new one's.
|
|
234
|
+
const before = normalize(existing);
|
|
235
|
+
const oldLength = placed.replaced === null ? 0 : normalize(placed.replaced).length;
|
|
236
|
+
const lineNow = (pos) => before.slice(0, pos < start ? pos : pos - end + start + oldLength).split('\n').length;
|
|
237
|
+
for (const { statement, prior } of readTomlDeclarations(normalize(placed.text)).conflicts) {
|
|
238
|
+
if (!inBlock(statement) && !inBlock(prior)) continue;
|
|
239
|
+
const theirs = inBlock(statement) ? prior : statement;
|
|
240
|
+
return { ours: inBlock(statement) ? statement : prior, line: theirs && !inBlock(theirs) ? lineNow(theirs.pos) : null };
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The error for an update blockConflict refused. It names the block's own
|
|
247
|
+
* table or key (hiveku-sync's names) and only the line number of the user's,
|
|
248
|
+
* so nothing the user typed is repeated (a pasted secret, say).
|
|
249
|
+
*/
|
|
250
|
+
function refusalMessage(tomlPath, existing, { ours, line }) {
|
|
251
|
+
const name =
|
|
252
|
+
ours.type === 'keyval' ? `the key ${formatKey([...ours.header, ...ours.key])}` : `the table [${formatKey(ours.key)}]`;
|
|
253
|
+
const where = line ? `line ${line} of it` : 'a line of it';
|
|
254
|
+
const lines = [
|
|
255
|
+
`${tomlPath} was not updated: ${where} conflicts with ${name} in the hiveku-sync block, and a TOML file may declare each table and key only once. Nothing in the folder was changed.`,
|
|
256
|
+
];
|
|
257
|
+
const begin = existing.indexOf(TOML_BEGIN);
|
|
258
|
+
if (begin !== -1 && existing.indexOf(TOML_BEGIN, begin + 1) !== -1) {
|
|
259
|
+
lines.push('The file has more than one "# hiveku:begin" line, so hiveku-sync could not tell which block is its own. Keep one block, then run `hiveku-sync codex` again.');
|
|
260
|
+
} else if (begin !== -1 && !(existing.indexOf(TOML_END) > begin)) {
|
|
261
|
+
lines.push('hiveku-sync could not find its own block to replace: the "# hiveku:begin" line has no "# hiveku:end" line after it. Put "# hiveku:end" back at the end of the block, then run `hiveku-sync codex` again.');
|
|
262
|
+
} else {
|
|
263
|
+
lines.push(`Remove or change ${line ? `line ${line}` : 'that line'}, then run \`hiveku-sync codex\` again.`);
|
|
264
|
+
}
|
|
265
|
+
return lines.join(' ');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** A TOML basic string holding `value`: quotes, backslashes and control characters other than tab escaped. */
|
|
269
|
+
function tomlString(value) {
|
|
270
|
+
const escaped = value.replace(/["\\\x00-\x08\x0a-\x1f\x7f]/g, (ch) =>
|
|
271
|
+
ch === '"' || ch === '\\' ? `\\${ch}` : `\\u${ch.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')}`,
|
|
272
|
+
);
|
|
273
|
+
return `"${escaped}"`;
|
|
274
|
+
}
|
|
275
|
+
|
|
102
276
|
/** The [mcp_servers.*] region for <folder>/.codex/config.toml. */
|
|
103
|
-
function codexTomlRegion({ baseUrl, profile, apiKey }) {
|
|
277
|
+
function codexTomlRegion({ baseUrl, profile, apiKey, userTables = [] }) {
|
|
104
278
|
const base = baseUrl.replace(/\/+$/, '');
|
|
105
279
|
const url = profile === 'full' ? `${base}/mcp` : `${base}/mcp?profile=${encodeURIComponent(profile)}`;
|
|
106
280
|
// Prefer an inlined key (self-contained, no env setup needed; the file is
|
|
@@ -113,20 +287,38 @@ function codexTomlRegion({ baseUrl, profile, apiKey }) {
|
|
|
113
287
|
// to bearer_token_env_var). TOML allows the key once per table, so the
|
|
114
288
|
// client label rides in the same inline table as an inlined key.
|
|
115
289
|
// X-Hiveku-Client: codex is what the MCP server's memory log shows as
|
|
116
|
-
// "via Codex"; it is a label, never authentication.
|
|
290
|
+
// "via Codex"; it is a label, never authentication. The key and the URL come
|
|
291
|
+
// from .env, so they are escaped: a quote in either would otherwise end the
|
|
292
|
+
// string early and leave the block itself unreadable.
|
|
117
293
|
const auth = apiKey
|
|
118
|
-
? `http_headers = { "Authorization" =
|
|
294
|
+
? `http_headers = { "Authorization" = ${tomlString(`Bearer ${apiKey}`)}, "X-Hiveku-Client" = "codex" }`
|
|
119
295
|
: `bearer_token_env_var = "OLYMPUS_API_KEY"\nhttp_headers = { "X-Hiveku-Client" = "codex" }`;
|
|
120
296
|
// default_tools_approval_mode = "approve": this folder exists so the agent
|
|
121
|
-
// can operate the account — per-call approval prompts would also
|
|
122
|
-
// every tool call in headless `codex exec` (CI/cron
|
|
123
|
-
//
|
|
297
|
+
// can operate the account — per-call approval prompts would also cancel
|
|
298
|
+
// every tool call in headless `codex exec` (CI/cron, this lane's core use)
|
|
299
|
+
// under a read-only or workspace-write sandbox.
|
|
300
|
+
// The per-tool tables after it override that default for CODEX_PROMPT_TOOLS
|
|
301
|
+
// (a tool's own approval_mode wins over the server default), so those ask
|
|
302
|
+
// first, exactly as they do under the Hiveku Codex plugin, in the modes where
|
|
303
|
+
// Codex asks at all (see CODEX_PROMPT_TOOLS). Each tool gets ONE table: a
|
|
304
|
+
// table defined twice makes the whole file invalid TOML, so merge any other
|
|
305
|
+
// prompt list into CODEX_PROMPT_TOOLS instead of adding tables here, and
|
|
306
|
+
// leave out the tools whose table the user declares outside this block
|
|
307
|
+
// (userTables): their own setting wins.
|
|
308
|
+
const skip = new Set(userTables);
|
|
309
|
+
const promptTables = CODEX_PROMPT_TOOLS.filter((tool) => !skip.has(tool))
|
|
310
|
+
.map((tool) => `[mcp_servers.hiveku.tools.${tomlKey(tool)}]\napproval_mode = "prompt"`)
|
|
311
|
+
.join('\n');
|
|
124
312
|
return `${TOML_BEGIN}
|
|
125
313
|
[mcp_servers.hiveku]
|
|
126
|
-
url =
|
|
314
|
+
url = ${tomlString(url)}
|
|
127
315
|
${auth}
|
|
128
316
|
default_tools_approval_mode = "approve"
|
|
129
317
|
|
|
318
|
+
# Ask first: a fixed list of high-risk tools (the Hiveku Codex plugin's list).
|
|
319
|
+
# Every other hiveku tool runs without asking.
|
|
320
|
+
${promptTables}
|
|
321
|
+
|
|
130
322
|
[mcp_servers.playwright]
|
|
131
323
|
command = "npx"
|
|
132
324
|
args = ["-y", "@playwright/mcp@latest"]
|
|
@@ -211,6 +403,10 @@ export async function mirrorKnowledgeToSkills(folder) {
|
|
|
211
403
|
* .codex/config.toml (git-ignored) + mirror the account's knowledge to skills.
|
|
212
404
|
* Resolves OLYMPUS_API_KEY from the folder's .env / process env to inline the
|
|
213
405
|
* MCP auth when possible.
|
|
406
|
+
*
|
|
407
|
+
* Throws, with nothing in the folder written, when the hiveku-sync block would
|
|
408
|
+
* declare a table or key that the rest of config.toml also declares (see
|
|
409
|
+
* blockConflict). The updated file is checked in memory, before any write.
|
|
214
410
|
*/
|
|
215
411
|
export async function writeCodexScaffold({ folder, slug, baseUrl, profile }) {
|
|
216
412
|
// The FOLDER's .env wins over a process-level export: folder = tenant
|
|
@@ -222,6 +418,24 @@ export async function writeCodexScaffold({ folder, slug, baseUrl, profile }) {
|
|
|
222
418
|
dotenv.OLYMPUS_API_KEY && dotenv.OLYMPUS_API_KEY !== 'olp_replace_me' ? dotenv.OLYMPUS_API_KEY : '';
|
|
223
419
|
const apiKey = dotenvKey || (existsSync(join(folder, '.env')) ? '' : process.env.OLYMPUS_API_KEY || '');
|
|
224
420
|
|
|
421
|
+
// .codex/config.toml (marker-managed region; rest of user's config untouched).
|
|
422
|
+
// First, and checked in memory before anything is written, so that a refused
|
|
423
|
+
// update leaves the whole folder as it was (no .codex/ folder created either).
|
|
424
|
+
const tomlPath = join(folder, '.codex', 'config.toml');
|
|
425
|
+
let toml = '';
|
|
426
|
+
try {
|
|
427
|
+
toml = await readFile(tomlPath, 'utf8');
|
|
428
|
+
} catch (err) {
|
|
429
|
+
if (err.code !== 'ENOENT') throw err;
|
|
430
|
+
}
|
|
431
|
+
const userTables = userToolTables(toml);
|
|
432
|
+
const region = codexTomlRegion({ baseUrl, profile, apiKey, userTables: userTables.tools });
|
|
433
|
+
const placed = placeMarkedRegion(toml, TOML_BEGIN, TOML_END, region);
|
|
434
|
+
const conflict = blockConflict(toml, placed, region);
|
|
435
|
+
if (conflict) throw new Error(refusalMessage(tomlPath, toml, conflict));
|
|
436
|
+
await mkdir(dirname(tomlPath), { recursive: true });
|
|
437
|
+
await writeFile(tomlPath, placed.text, 'utf8');
|
|
438
|
+
|
|
225
439
|
// AGENTS.md (marker-managed)
|
|
226
440
|
const agentsPath = join(folder, 'AGENTS.md');
|
|
227
441
|
let agents = '';
|
|
@@ -232,17 +446,6 @@ export async function writeCodexScaffold({ folder, slug, baseUrl, profile }) {
|
|
|
232
446
|
}
|
|
233
447
|
await writeFile(agentsPath, upsertMarkedRegion(agents, AGENTS_BEGIN, AGENTS_END, agentsMdRegion({ slug })), 'utf8');
|
|
234
448
|
|
|
235
|
-
// .codex/config.toml (marker-managed region; rest of user's config untouched)
|
|
236
|
-
const tomlPath = join(folder, '.codex', 'config.toml');
|
|
237
|
-
let toml = '';
|
|
238
|
-
try {
|
|
239
|
-
toml = await readFile(tomlPath, 'utf8');
|
|
240
|
-
} catch {
|
|
241
|
-
/* new */
|
|
242
|
-
}
|
|
243
|
-
await mkdir(dirname(tomlPath), { recursive: true });
|
|
244
|
-
await writeFile(tomlPath, upsertMarkedRegion(toml, TOML_BEGIN, TOML_END, codexTomlRegion({ baseUrl, profile, apiKey })), 'utf8');
|
|
245
|
-
|
|
246
449
|
// Gitignore EVERY credential-bearing file this lane depends on — not just the
|
|
247
450
|
// one we write. The retrofit reads .env for OLYMPUS_API_KEY to inline it, so a
|
|
248
451
|
// folder that had no .gitignore would otherwise end up with one that ignores
|
|
@@ -253,7 +456,14 @@ export async function writeCodexScaffold({ folder, slug, baseUrl, profile }) {
|
|
|
253
456
|
await appendGitignoreLines(folder, ['.codex/config.toml', '.env', '.env.local', '.hiveku/']);
|
|
254
457
|
|
|
255
458
|
const skills = await mirrorKnowledgeToSkills(folder);
|
|
256
|
-
|
|
459
|
+
// userToolTables: listed tools left to the user's own table (the CLI names
|
|
460
|
+
// them); userTablesWithoutApprovalMode: those whose table sets no approval_mode.
|
|
461
|
+
return {
|
|
462
|
+
inlinedKey: !!apiKey,
|
|
463
|
+
skillsMirrored: skills,
|
|
464
|
+
userToolTables: userTables.tools,
|
|
465
|
+
userTablesWithoutApprovalMode: userTables.withoutApprovalMode,
|
|
466
|
+
};
|
|
257
467
|
}
|
|
258
468
|
|
|
259
469
|
/**
|