@hiveku-apps/sync 0.5.17 → 0.5.19
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 +18 -4
- package/bin/hiveku-sync.mjs +30 -3
- package/package.json +1 -1
- package/src/codex-prompt-tools.json +5 -1
- package/src/codex.mjs +186 -17
- package/src/init.mjs +6 -8
- package/src/pull.mjs +45 -1
package/README.md
CHANGED
|
@@ -12,7 +12,9 @@ Local sync CLI for a Hiveku account. Pairs with the Hiveku MCP server at
|
|
|
12
12
|
It also writes the account memory (see "Account memory" below) to
|
|
13
13
|
`hiveku-data/account/ACCOUNT_MEMORY.md`, read-only, and brings the folder
|
|
14
14
|
layout, the memory rules and the plan rules in `CLAUDE.md` up to date (see
|
|
15
|
-
"CLAUDE.md stays current" below).
|
|
15
|
+
"CLAUDE.md stays current" below). In a Codex folder it also brings the
|
|
16
|
+
ask-first prompts in `.codex/config.toml` up to date (see "Codex (OpenAI)
|
|
17
|
+
support" below).
|
|
16
18
|
|
|
17
19
|
The goal: open one folder per Hiveku tenant in VS Code with Claude Code,
|
|
18
20
|
with the account's memory mirrored as local `.md` files Claude can read
|
|
@@ -126,12 +128,24 @@ the last `pull` wrote under `rules/<agent>/` (never those in
|
|
|
126
128
|
`rules/<agent>/not-followed/`). Run `hiveku-sync codex` after `pull` to refresh it
|
|
127
129
|
(`pull` reminds you when the folder has a Codex lane).
|
|
128
130
|
|
|
131
|
+
**`pull` keeps the ask-first prompts current.** When `.codex/config.toml` has
|
|
132
|
+
the hiveku-sync block, every `pull` brings that block up to date exactly as
|
|
133
|
+
`hiveku-sync codex` would write it, so a tool added to the ask-first list asks
|
|
134
|
+
in the folder after its next pull, and the pull names the tools that ask now.
|
|
135
|
+
It takes the key from `.env` as `hiveku-sync codex` does, and says so when that
|
|
136
|
+
changes the key Codex uses. The rules below still hold: your own tool tables
|
|
137
|
+
win, and a block that would declare a table or key twice is not written. Then
|
|
138
|
+
the pull ends with an error (exit 2) that names the line to fix and the listed
|
|
139
|
+
tools that run without asking until you do. `pull` never creates
|
|
140
|
+
`.codex/config.toml` or a block in it, and it leaves `AGENTS.md` and
|
|
141
|
+
`.agents/skills/` to `hiveku-sync codex`.
|
|
142
|
+
|
|
129
143
|
Pass `--trust` to pre-trust the folder in `~/.codex/config.toml` (so Codex loads
|
|
130
144
|
the repo config without its first-run trust prompt).
|
|
131
145
|
|
|
132
146
|
**Approvals.** Codex asks first only for a **fixed list** of high-risk Hiveku
|
|
133
|
-
tools: the
|
|
134
|
-
Hiveku Codex plugin 0.3.
|
|
147
|
+
tools: the 331 names in `src/codex-prompt-tools.json`, the same list as the
|
|
148
|
+
Hiveku Codex plugin 0.3.47. It includes `deploy_site`, `site_delete`,
|
|
135
149
|
`project_files_bulk_save` and every `hiveku_batch`, and it is written as
|
|
136
150
|
`[mcp_servers.hiveku.tools.<tool>] approval_mode = "prompt"`. **Every other tool
|
|
137
151
|
on the `hiveku` server runs without asking** (`default_tools_approval_mode =
|
|
@@ -181,7 +195,7 @@ Headless and bypass modes:
|
|
|
181
195
|
|
|
182
196
|
Folders set up by a hiveku-sync without these prompts (0.3.5 and earlier)
|
|
183
197
|
approve every tool: run `hiveku-sync codex` in them once, with a release that has
|
|
184
|
-
them, to add the prompts.
|
|
198
|
+
them, to add the prompts. From 0.5.18 a `pull` adds them too.
|
|
185
199
|
|
|
186
200
|
> **This vs. the plugin.** For a single account, you can instead install the
|
|
187
201
|
> published [Hiveku Codex plugin](https://github.com/locus2012/hiveku-codex-plugin)
|
package/bin/hiveku-sync.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { runInit } from '../src/init.mjs';
|
|
12
12
|
import { runCodex } from '../src/init.mjs';
|
|
13
|
-
import { runPull } from '../src/pull.mjs';
|
|
13
|
+
import { runPull, toolNames } from '../src/pull.mjs';
|
|
14
14
|
import {
|
|
15
15
|
runProjectsList,
|
|
16
16
|
runPullProject,
|
|
@@ -86,8 +86,10 @@ Memory/rules/skills:
|
|
|
86
86
|
that owns each one (<type>/<agent>/, and
|
|
87
87
|
<type>/shared/ for every agent). Brings the
|
|
88
88
|
folder layout and memory rules in CLAUDE.md
|
|
89
|
-
up to date
|
|
90
|
-
|
|
89
|
+
up to date, and in a Codex folder the
|
|
90
|
+
ask-first prompts in .codex/config.toml.
|
|
91
|
+
Reads OLYMPUS_API_KEY and OLYMPUS_BASE_URL
|
|
92
|
+
from ./.env.
|
|
91
93
|
codex [<folder>] [--trust] Retrofit an existing account folder with the
|
|
92
94
|
Codex lane (AGENTS.md + .codex/config.toml +
|
|
93
95
|
mirror the account's skills/commands to
|
|
@@ -193,6 +195,30 @@ function userToolTablesLines({ userToolTables = [], userTablesWithoutApprovalMod
|
|
|
193
195
|
return lines;
|
|
194
196
|
}
|
|
195
197
|
|
|
198
|
+
/**
|
|
199
|
+
* What `pull` did to the hiveku-sync block of .codex/config.toml
|
|
200
|
+
* (refreshCodexConfig), as lines to print: only when it wrote the block. A
|
|
201
|
+
* block it could not write is printed with the errors, and a current block or
|
|
202
|
+
* a folder with none says nothing.
|
|
203
|
+
*/
|
|
204
|
+
export function codexConfigLines(result) {
|
|
205
|
+
if (result?.status !== 'updated') return [];
|
|
206
|
+
const from = result.release ? ` (hiveku-sync ${result.release} wrote it)` : '';
|
|
207
|
+
const lines = [` Codex: brought the hiveku-sync block in .codex/config.toml up to date${from}.`];
|
|
208
|
+
const added = result.added ?? [];
|
|
209
|
+
if (added.length) {
|
|
210
|
+
lines.push(` It now also asks first before ${toolNames(added)}.`);
|
|
211
|
+
}
|
|
212
|
+
const AUTH = {
|
|
213
|
+
inlined: 'MCP auth: it now carries the OLYMPUS_API_KEY from .env (it read the OLYMPUS_API_KEY env var before).',
|
|
214
|
+
'env-var': 'MCP auth: it now reads the OLYMPUS_API_KEY env var, because .env has no key to put in it.',
|
|
215
|
+
key: 'MCP auth: it now carries the OLYMPUS_API_KEY from .env, which is not the key it held before.',
|
|
216
|
+
};
|
|
217
|
+
if (AUTH[result.auth]) lines.push(` ${AUTH[result.auth]}`);
|
|
218
|
+
for (const line of userToolTablesLines(result)) lines.push(` ${line}`);
|
|
219
|
+
return lines;
|
|
220
|
+
}
|
|
221
|
+
|
|
196
222
|
/**
|
|
197
223
|
* What refreshing CLAUDE.md's kept-current part (folder layout and memory
|
|
198
224
|
* rules, claude-md.mjs) did, as lines to print. Nothing when it was already
|
|
@@ -339,6 +365,7 @@ async function main() {
|
|
|
339
365
|
console.log('They were left on disk. Delete them manually if no longer needed.');
|
|
340
366
|
}
|
|
341
367
|
for (const line of claudeMdLines(summary.claude_md)) console.log(line);
|
|
368
|
+
for (const line of codexConfigLines(summary.codex_config)) console.log(line);
|
|
342
369
|
if (summary.codex_lane) {
|
|
343
370
|
console.log('\nCodex: run `hiveku-sync codex` to refresh the rules list in AGENTS.md and the skills in .agents/skills/.');
|
|
344
371
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hiveku-apps/sync",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.19",
|
|
4
4
|
"description": "Sync a Hiveku account's memory/rules/skills + coder projects into a local folder, and ship projects (push/deploy/pull-env) from the terminal. Pairs with the Hiveku MCP at core.hiveku.com/mcp so Claude Code can drive Hiveku tools per-folder.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"source": "locus2012/hiveku-codex-plugin 0.3.
|
|
2
|
+
"source": "locus2012/hiveku-codex-plugin 0.3.47 (feature/ask-contract-alerts 5ff2b8e, on main c535f59), plugins/hiveku/.mcp.json: every hiveku.tools entry with approval_mode \"prompt\" (project_vcs_rollback included)",
|
|
3
3
|
"why": "hiveku-sync writes its own [mcp_servers.hiveku] into a Codex folder. Codex then uses that entry, not the plugin's, for the server's connection and for every tool's approval mode, so these prompts must be written here too. Refresh this list whenever the Codex plugin's prompt list changes.",
|
|
4
4
|
"tools": [
|
|
5
5
|
"deploy_site",
|
|
@@ -77,6 +77,9 @@
|
|
|
77
77
|
"email_project_connect",
|
|
78
78
|
"email_suppression_import",
|
|
79
79
|
"crm_envelope_send",
|
|
80
|
+
"crm_envelope_alerts_set",
|
|
81
|
+
"crm_contract_template_create",
|
|
82
|
+
"crm_contract_template_update",
|
|
80
83
|
"crm_estimate_send",
|
|
81
84
|
"helpdesk_ticket_send_reply",
|
|
82
85
|
"helpdesk_assistant_knowledge_sources_update",
|
|
@@ -185,6 +188,7 @@
|
|
|
185
188
|
"voice_port_order_create",
|
|
186
189
|
"voice_port_order_action",
|
|
187
190
|
"voice_port_order_document_upload",
|
|
191
|
+
"voice_port_order_loa_send",
|
|
188
192
|
"voice_settings_update",
|
|
189
193
|
"voice_number_update",
|
|
190
194
|
"voice_blocked_numbers_remove",
|
package/src/codex.mjs
CHANGED
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
* without asking. The safe-work rules are stated
|
|
28
28
|
* as instructions in AGENTS.md, for every tool.
|
|
29
29
|
*
|
|
30
|
+
* `hiveku-sync pull` rewrites the .codex/config.toml block too, when the file
|
|
31
|
+
* has one (refreshCodexConfig, the same step as `hiveku-sync codex`), so a
|
|
32
|
+
* tool added to the ask-first list asks in an existing folder after its next
|
|
33
|
+
* pull, not only after someone remembers to run `hiveku-sync codex`.
|
|
34
|
+
*
|
|
30
35
|
* This complements the published Hiveku Codex plugin
|
|
31
36
|
* (github.com/locus2012/hiveku-codex-plugin): the plugin ships the generic
|
|
32
37
|
* `hiveku` MCP server + doctrine skills for single-account users who install it
|
|
@@ -98,6 +103,20 @@ export const CODEX_PROMPT_TOOLS = [
|
|
|
98
103
|
/** This package's version, as published (package.json ships in every npm package). */
|
|
99
104
|
export const SYNC_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
100
105
|
|
|
106
|
+
/** The Hiveku MCP host a folder uses when its .env names none. */
|
|
107
|
+
export const DEFAULT_BASE_URL = 'https://core.hiveku.com';
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The URL and profile a folder's Codex lane is written with: the folder's own
|
|
111
|
+
* .env (OLYMPUS_BASE_URL, HIVEKU_PROFILE), else the defaults. `hiveku-sync
|
|
112
|
+
* codex` and `pull` both read them here, so a pull writes the block exactly as
|
|
113
|
+
* `hiveku-sync codex` would.
|
|
114
|
+
*/
|
|
115
|
+
export async function codexLaneSettings(folder) {
|
|
116
|
+
const dotenv = await loadDotenv(join(folder, '.env'));
|
|
117
|
+
return { baseUrl: dotenv.OLYMPUS_BASE_URL || DEFAULT_BASE_URL, profile: dotenv.HIVEKU_PROFILE || 'full' };
|
|
118
|
+
}
|
|
119
|
+
|
|
101
120
|
/**
|
|
102
121
|
* X-Hiveku-Client for the folder's [mcp_servers.hiveku]: `hiveku-sync/<version>`,
|
|
103
122
|
* the declaration that this config asks a person before project_secrets_reveal
|
|
@@ -300,13 +319,16 @@ function blockConflict(existing, placed, region) {
|
|
|
300
319
|
* The error for an update blockConflict refused. It names the block's own
|
|
301
320
|
* table or key (hiveku-sync's names) and only the line number of the user's,
|
|
302
321
|
* so nothing the user typed is repeated (a pasted secret, say).
|
|
322
|
+
* `folderUnchanged`: `hiveku-sync codex` writes nothing in the folder when it
|
|
323
|
+
* refuses, and says so; `pull` has written the pulled files by then, so it
|
|
324
|
+
* passes false.
|
|
303
325
|
*/
|
|
304
|
-
function refusalMessage(tomlPath, existing, { ours, line }) {
|
|
326
|
+
function refusalMessage(tomlPath, existing, { ours, line }, { folderUnchanged = true } = {}) {
|
|
305
327
|
const name =
|
|
306
328
|
ours.type === 'keyval' ? `the key ${formatKey([...ours.header, ...ours.key])}` : `the table [${formatKey(ours.key)}]`;
|
|
307
329
|
const where = line ? `line ${line} of it` : 'a line of it';
|
|
308
330
|
const lines = [
|
|
309
|
-
`${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
|
|
331
|
+
`${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.${folderUnchanged ? ' Nothing in the folder was changed.' : ''}`,
|
|
310
332
|
];
|
|
311
333
|
const begin = existing.indexOf(TOML_BEGIN);
|
|
312
334
|
if (begin !== -1 && existing.indexOf(TOML_BEGIN, begin + 1) !== -1) {
|
|
@@ -624,25 +646,175 @@ export async function rulesDigest(folder) {
|
|
|
624
646
|
return [...head, ...lines].join('\n');
|
|
625
647
|
}
|
|
626
648
|
|
|
649
|
+
/**
|
|
650
|
+
* The key a folder's block inlines, or '' to read the OLYMPUS_API_KEY env var.
|
|
651
|
+
* The FOLDER's .env wins over a process-level export: folder = tenant
|
|
652
|
+
* boundary, and a globally exported key for account A must never get inlined
|
|
653
|
+
* into account B's config during a retrofit. process.env is only a fallback
|
|
654
|
+
* for folders that keep no .env at all.
|
|
655
|
+
*/
|
|
656
|
+
async function folderApiKey(folder) {
|
|
657
|
+
const dotenv = await loadDotenv(join(folder, '.env'));
|
|
658
|
+
const dotenvKey =
|
|
659
|
+
dotenv.OLYMPUS_API_KEY && dotenv.OLYMPUS_API_KEY !== 'olp_replace_me' ? dotenv.OLYMPUS_API_KEY : '';
|
|
660
|
+
return dotenvKey || (existsSync(join(folder, '.env')) ? '' : process.env.OLYMPUS_API_KEY || '');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/** Every credential-bearing file the Codex lane depends on (see writeCodexScaffold). */
|
|
664
|
+
const CREDENTIAL_IGNORES = ['.codex/config.toml', '.env', '.env.local', '.hiveku/'];
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* The one .codex/config.toml step, shared by `hiveku-sync codex` (through
|
|
668
|
+
* writeCodexScaffold) and `pull` (refreshCodexConfig): `existing` with the
|
|
669
|
+
* hiveku-sync block replaced (or appended) for this release, checked in
|
|
670
|
+
* memory and not written. Listed tools whose table the user declares outside
|
|
671
|
+
* the block are left to that table (userToolTables). `conflict` is set
|
|
672
|
+
* (blockConflict) when the block would declare a table or key that the rest
|
|
673
|
+
* of the file also declares: then the text must not be written.
|
|
674
|
+
*/
|
|
675
|
+
function planCodexConfig({ existing, baseUrl, profile, apiKey }) {
|
|
676
|
+
const userTables = userToolTables(existing);
|
|
677
|
+
const region = codexTomlRegion({ baseUrl, profile, apiKey, userTables: userTables.tools });
|
|
678
|
+
const placed = placeMarkedRegion(existing, TOML_BEGIN, TOML_END, region);
|
|
679
|
+
return { text: placed.text, userTables, conflict: blockConflict(existing, placed, region) };
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* The tools that `text` gives a prompt table the way hiveku-sync writes one in
|
|
684
|
+
* its block, `[mcp_servers.hiveku.tools.<tool>]` followed by
|
|
685
|
+
* `approval_mode = "prompt"`, wherever they are in the file.
|
|
686
|
+
*/
|
|
687
|
+
function promptTables(text) {
|
|
688
|
+
const asked = new Set();
|
|
689
|
+
for (const hit of text.replace(/\r\n/g, '\n').matchAll(/^\[mcp_servers\.hiveku\.tools\.([A-Za-z0-9_-]+)\]\napproval_mode = "prompt"$/gm)) {
|
|
690
|
+
asked.add(hit[1]);
|
|
691
|
+
}
|
|
692
|
+
return asked;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** The hiveku-sync block in `text`, to its end line or the end of the file; '' when there is none. */
|
|
696
|
+
function blockText(text) {
|
|
697
|
+
const begin = text.indexOf(TOML_BEGIN);
|
|
698
|
+
if (begin === -1) return '';
|
|
699
|
+
const end = text.indexOf(TOML_END, begin);
|
|
700
|
+
return text.slice(begin, end === -1 ? undefined : end);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* The hiveku-sync release the block in `text` declares in X-Hiveku-Client
|
|
705
|
+
* (`hiveku-sync/<version>`), or null: a block that sends the plain `codex`
|
|
706
|
+
* label, or one an older release wrote without the header, does not say.
|
|
707
|
+
*/
|
|
708
|
+
function blockRelease(text) {
|
|
709
|
+
return blockText(text).match(/"X-Hiveku-Client" = "hiveku-sync\/([0-9A-Za-z.+-]+)"/)?.[1] ?? null;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* How the block in `text` authenticates: its inlined Authorization value, or
|
|
714
|
+
* 'env' when it reads the OLYMPUS_API_KEY env var. Compared in memory only,
|
|
715
|
+
* never returned or printed: the value is the account's key.
|
|
716
|
+
*/
|
|
717
|
+
function blockAuth(text) {
|
|
718
|
+
const block = blockText(text);
|
|
719
|
+
const inlined = block.match(/"Authorization" = ("(?:[^"\\]|\\.)*")/);
|
|
720
|
+
if (inlined) return inlined[1];
|
|
721
|
+
return /^bearer_token_env_var = "OLYMPUS_API_KEY"$/m.test(block) ? 'env' : null;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* What a rewrite changed in how the folder's Codex reaches Hiveku, so `pull`
|
|
726
|
+
* can say it (the key comes from the folder's .env, as for `hiveku-sync
|
|
727
|
+
* codex`): 'inlined' (it read the env var before), 'env-var' (it inlined a
|
|
728
|
+
* key before, and .env has none to inline now), 'key' (another key), or null.
|
|
729
|
+
*/
|
|
730
|
+
function authChange(before, after) {
|
|
731
|
+
const was = blockAuth(before);
|
|
732
|
+
const now = blockAuth(after);
|
|
733
|
+
if (was === now || now === null) return null;
|
|
734
|
+
if (now === 'env') return 'env-var';
|
|
735
|
+
return was === 'env' ? 'inlined' : 'key';
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* `hiveku-sync pull` in a Codex folder: rewrite the hiveku-sync block of
|
|
740
|
+
* .codex/config.toml exactly as `hiveku-sync codex` would (planCodexConfig:
|
|
741
|
+
* the user's own tool tables win, and a block that would declare a table or
|
|
742
|
+
* key twice is refused with nothing written). Without this, a folder kept the
|
|
743
|
+
* prompt list of the release that last ran `hiveku-sync codex` there, and its
|
|
744
|
+
* own [mcp_servers.hiveku] approves every other tool, so a tool added to the
|
|
745
|
+
* list since ran without asking (review of #29, 2026-10-29).
|
|
746
|
+
*
|
|
747
|
+
* It never creates the file or a block: a folder with neither has no
|
|
748
|
+
* [mcp_servers.hiveku] of hiveku-sync's for Codex to use. It does not touch
|
|
749
|
+
* AGENTS.md or the mirrored skills (`hiveku-sync codex` does).
|
|
750
|
+
*
|
|
751
|
+
* Returns { status } and what to say:
|
|
752
|
+
* 'no-block' no .codex/config.toml, or no hiveku-sync block in it;
|
|
753
|
+
* 'current' the block is what this release writes; nothing written;
|
|
754
|
+
* 'updated' written. `added`: listed tools that ask now and did not
|
|
755
|
+
* before; `release`: the hiveku-sync that wrote the old block;
|
|
756
|
+
* `auth`: authChange's word, or null; userToolTables /
|
|
757
|
+
* userTablesWithoutApprovalMode as writeCodexScaffold returns
|
|
758
|
+
* them;
|
|
759
|
+
* 'refused' blockConflict: nothing written. `message` says why and what
|
|
760
|
+
* to do; `notAsking`: listed tools the file gives no prompt and
|
|
761
|
+
* no table of the user's own, when Codex can read the file as it
|
|
762
|
+
* is (null when a line cannot be read or something is declared
|
|
763
|
+
* twice already, so Codex cannot load it); `release` as above;
|
|
764
|
+
* 'failed' the file could not be read or written: `message`.
|
|
765
|
+
*/
|
|
766
|
+
export async function refreshCodexConfig(folder) {
|
|
767
|
+
const tomlPath = join(folder, '.codex', 'config.toml');
|
|
768
|
+
let existing;
|
|
769
|
+
try {
|
|
770
|
+
existing = await readFile(tomlPath, 'utf8');
|
|
771
|
+
} catch (err) {
|
|
772
|
+
if (err.code === 'ENOENT') return { status: 'no-block' };
|
|
773
|
+
return { status: 'failed', message: `${tomlPath} could not be read: ${err.message}` };
|
|
774
|
+
}
|
|
775
|
+
if (!existing.includes(TOML_BEGIN)) return { status: 'no-block' };
|
|
776
|
+
const { baseUrl, profile } = await codexLaneSettings(folder);
|
|
777
|
+
const apiKey = await folderApiKey(folder);
|
|
778
|
+
const asked = promptTables(existing);
|
|
779
|
+
const release = blockRelease(existing);
|
|
780
|
+
const plan = planCodexConfig({ existing, baseUrl, profile, apiKey });
|
|
781
|
+
if (plan.conflict) {
|
|
782
|
+
const message = refusalMessage(tomlPath, existing, plan.conflict, { folderUnchanged: false });
|
|
783
|
+
const { conflicts, unreadLines } = readTomlDeclarations(existing.replace(/\r\n/g, '\n'));
|
|
784
|
+
const readable = conflicts.length === 0 && unreadLines.length === 0;
|
|
785
|
+
const own = new Set(userToolTables(existing).tools);
|
|
786
|
+
const notAsking = readable ? CODEX_PROMPT_TOOLS.filter((tool) => !asked.has(tool) && !own.has(tool)) : null;
|
|
787
|
+
return { status: 'refused', message, notAsking, release };
|
|
788
|
+
}
|
|
789
|
+
const tables = {
|
|
790
|
+
userToolTables: plan.userTables.tools,
|
|
791
|
+
userTablesWithoutApprovalMode: plan.userTables.withoutApprovalMode,
|
|
792
|
+
};
|
|
793
|
+
if (plan.text === existing) return { status: 'current', release, ...tables };
|
|
794
|
+
try {
|
|
795
|
+
// .gitignore first, as `hiveku-sync codex` keeps it: the block may now inline the key.
|
|
796
|
+
await appendGitignoreLines(folder, CREDENTIAL_IGNORES);
|
|
797
|
+
await writeFile(tomlPath, plan.text, 'utf8');
|
|
798
|
+
} catch (err) {
|
|
799
|
+
return { status: 'failed', message: `${tomlPath} was not updated: ${err.message}` };
|
|
800
|
+
}
|
|
801
|
+
const own = new Set(plan.userTables.tools);
|
|
802
|
+
const added = CODEX_PROMPT_TOOLS.filter((tool) => !asked.has(tool) && !own.has(tool));
|
|
803
|
+
return { status: 'updated', added, release, auth: authChange(existing, plan.text), ...tables };
|
|
804
|
+
}
|
|
805
|
+
|
|
627
806
|
/**
|
|
628
807
|
* Write/refresh the Codex scaffold for one account folder: AGENTS.md +
|
|
629
808
|
* .codex/config.toml (git-ignored) + mirror the account's knowledge to skills.
|
|
630
809
|
* Resolves OLYMPUS_API_KEY from the folder's .env / process env to inline the
|
|
631
|
-
* MCP auth when possible.
|
|
810
|
+
* MCP auth when possible (folderApiKey).
|
|
632
811
|
*
|
|
633
812
|
* Throws, with nothing in the folder written, when the hiveku-sync block would
|
|
634
813
|
* declare a table or key that the rest of config.toml also declares (see
|
|
635
814
|
* blockConflict). The updated file is checked in memory, before any write.
|
|
636
815
|
*/
|
|
637
816
|
export async function writeCodexScaffold({ folder, slug, baseUrl, profile }) {
|
|
638
|
-
|
|
639
|
-
// boundary, and a globally exported key for account A must never get
|
|
640
|
-
// inlined into account B's config during a retrofit. process.env is only a
|
|
641
|
-
// fallback for folders that keep no .env at all.
|
|
642
|
-
const dotenv = await loadDotenv(join(folder, '.env'));
|
|
643
|
-
const dotenvKey =
|
|
644
|
-
dotenv.OLYMPUS_API_KEY && dotenv.OLYMPUS_API_KEY !== 'olp_replace_me' ? dotenv.OLYMPUS_API_KEY : '';
|
|
645
|
-
const apiKey = dotenvKey || (existsSync(join(folder, '.env')) ? '' : process.env.OLYMPUS_API_KEY || '');
|
|
817
|
+
const apiKey = await folderApiKey(folder);
|
|
646
818
|
|
|
647
819
|
// .codex/config.toml (marker-managed region; rest of user's config untouched).
|
|
648
820
|
// First, and checked in memory before anything is written, so that a refused
|
|
@@ -654,13 +826,10 @@ export async function writeCodexScaffold({ folder, slug, baseUrl, profile }) {
|
|
|
654
826
|
} catch (err) {
|
|
655
827
|
if (err.code !== 'ENOENT') throw err;
|
|
656
828
|
}
|
|
657
|
-
const userTables =
|
|
658
|
-
const region = codexTomlRegion({ baseUrl, profile, apiKey, userTables: userTables.tools });
|
|
659
|
-
const placed = placeMarkedRegion(toml, TOML_BEGIN, TOML_END, region);
|
|
660
|
-
const conflict = blockConflict(toml, placed, region);
|
|
829
|
+
const { text, userTables, conflict } = planCodexConfig({ existing: toml, baseUrl, profile, apiKey });
|
|
661
830
|
if (conflict) throw new Error(refusalMessage(tomlPath, toml, conflict));
|
|
662
831
|
await mkdir(dirname(tomlPath), { recursive: true });
|
|
663
|
-
await writeFile(tomlPath,
|
|
832
|
+
await writeFile(tomlPath, text, 'utf8');
|
|
664
833
|
|
|
665
834
|
// AGENTS.md (marker-managed)
|
|
666
835
|
const agentsPath = join(folder, 'AGENTS.md');
|
|
@@ -680,7 +849,7 @@ export async function writeCodexScaffold({ folder, slug, baseUrl, profile }) {
|
|
|
680
849
|
// entry point was a leaked credential; a scaffolder must never leave one
|
|
681
850
|
// git-trackable.) runInit already covers .env for fresh folders; this makes
|
|
682
851
|
// the retrofit path equally safe.
|
|
683
|
-
await appendGitignoreLines(folder,
|
|
852
|
+
await appendGitignoreLines(folder, CREDENTIAL_IGNORES);
|
|
684
853
|
|
|
685
854
|
const skills = await syncKnowledgeSkills(folder);
|
|
686
855
|
// userToolTables: listed tools left to the user's own table (the CLI names
|
package/src/init.mjs
CHANGED
|
@@ -13,12 +13,11 @@ import { existsSync } from 'node:fs';
|
|
|
13
13
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
14
14
|
import { basename, join, resolve } from 'node:path';
|
|
15
15
|
|
|
16
|
-
import { writeFileAtomic, safeSlug, TYPE_TO_FOLDER
|
|
17
|
-
import { writeCodexScaffold, preTrustFolder } from './codex.mjs';
|
|
16
|
+
import { writeFileAtomic, safeSlug, TYPE_TO_FOLDER } from './util.mjs';
|
|
17
|
+
import { DEFAULT_BASE_URL, codexLaneSettings, writeCodexScaffold, preTrustFolder } from './codex.mjs';
|
|
18
18
|
import { claudeMdMemoryRegion, refreshClaudeMd } from './claude-md.mjs';
|
|
19
19
|
|
|
20
20
|
const VALID_PROFILES = ['full', 'sales', 'marketing', 'helpdesk', 'pm', 'dev'];
|
|
21
|
-
const DEFAULT_BASE_URL = 'https://core.hiveku.com';
|
|
22
21
|
|
|
23
22
|
function dotenvTemplate({ profile, baseUrl }) {
|
|
24
23
|
return [
|
|
@@ -229,8 +228,9 @@ export async function runInit({ slug, parentDir, profile = 'full', codex = false
|
|
|
229
228
|
/**
|
|
230
229
|
* Retrofit an EXISTING account folder with the Codex lane (AGENTS.md +
|
|
231
230
|
* .codex/config.toml + knowledge→skills). Run from inside the folder, or pass
|
|
232
|
-
* one. Derives the account slug from the folder name and the profile
|
|
233
|
-
* .env (
|
|
231
|
+
* one. Derives the account slug from the folder name and the profile and URL
|
|
232
|
+
* from .env (codexLaneSettings, which `pull` reads too), and inlines the real
|
|
233
|
+
* OLYMPUS_API_KEY when .env has it.
|
|
234
234
|
* AGENTS.md sends Codex to CLAUDE.md first, so the part of CLAUDE.md that
|
|
235
235
|
* hiveku-sync keeps current is refreshed too (claude-md.mjs), after the
|
|
236
236
|
* scaffold is written: a refused config.toml update changes nothing.
|
|
@@ -238,9 +238,7 @@ export async function runInit({ slug, parentDir, profile = 'full', codex = false
|
|
|
238
238
|
export async function runCodex({ folder, trust = false } = {}) {
|
|
239
239
|
const target = folder ? resolve(folder) : process.cwd();
|
|
240
240
|
if (!existsSync(target)) throw new Error(`Folder not found: ${target}`);
|
|
241
|
-
const
|
|
242
|
-
const profile = dotenv.HIVEKU_PROFILE || 'full';
|
|
243
|
-
const baseUrl = dotenv.OLYMPUS_BASE_URL || DEFAULT_BASE_URL;
|
|
241
|
+
const { baseUrl, profile } = await codexLaneSettings(target);
|
|
244
242
|
const slug = safeSlug(basename(target));
|
|
245
243
|
const result = await writeCodexScaffold({ folder: target, slug, baseUrl, profile });
|
|
246
244
|
if (trust) result.trust = await preTrustFolder(target);
|
package/src/pull.mjs
CHANGED
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
* It also refreshes the folder layout and memory rules in CLAUDE.md, the part
|
|
17
17
|
* between hiveku-sync's markers (claude-md.mjs).
|
|
18
18
|
*
|
|
19
|
+
* In a Codex folder it rewrites the hiveku-sync block of .codex/config.toml
|
|
20
|
+
* the way `hiveku-sync codex` does (codex.mjs refreshCodexConfig), so a tool
|
|
21
|
+
* added to the ask-first list asks there after this pull. A block it cannot
|
|
22
|
+
* write is an error (exit 2): until it is fixed, a listed tool that block has
|
|
23
|
+
* no prompt for runs in that folder without asking.
|
|
24
|
+
*
|
|
19
25
|
* The account memory (domains `account` and `account-suggestions`) is not a
|
|
20
26
|
* knowledge entry: it is classified explicitly, never written under memory/,
|
|
21
27
|
* and pulled once from account_memory_get into a read-only
|
|
@@ -49,6 +55,7 @@ import {
|
|
|
49
55
|
} from './account-memory.mjs';
|
|
50
56
|
import { planKnowledgeFiles } from './knowledge-files.mjs';
|
|
51
57
|
import { refreshClaudeMd } from './claude-md.mjs';
|
|
58
|
+
import { refreshCodexConfig } from './codex.mjs';
|
|
52
59
|
|
|
53
60
|
const MANIFEST_PATH = '.hiveku/manifest.json';
|
|
54
61
|
export const KNOWLEDGE_SET_ASIDE_DIR = '.hiveku/set-aside/knowledge';
|
|
@@ -288,5 +295,42 @@ export async function runPull({ baseDir = process.cwd(), profile, client: inject
|
|
|
288
295
|
/* no Codex lane */
|
|
289
296
|
}
|
|
290
297
|
|
|
291
|
-
|
|
298
|
+
// The Codex folder's ask-first prompts: its own [mcp_servers.hiveku] approves
|
|
299
|
+
// every tool it gives no prompt table, so the block is brought up to this
|
|
300
|
+
// release's list here, not only when someone runs `hiveku-sync codex`.
|
|
301
|
+
const codexConfig = await refreshCodexConfig(baseDir);
|
|
302
|
+
if (codexConfig.status === 'refused' || codexConfig.status === 'failed') {
|
|
303
|
+
summary.errors.push({ type: 'codex config', message: codexConfigProblem(codexConfig) });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
...summary,
|
|
308
|
+
removed_remotely: removedRemotely,
|
|
309
|
+
profile: activeProfile,
|
|
310
|
+
baseUrl,
|
|
311
|
+
codex_lane: codexLane,
|
|
312
|
+
codex_config: codexConfig,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The error line for a config.toml block `pull` could not bring up to date:
|
|
318
|
+
* why (refreshCodexConfig's message, which says what to do), then, when Codex
|
|
319
|
+
* can read the file as it is, the listed tools that run there without asking
|
|
320
|
+
* until it is fixed.
|
|
321
|
+
*/
|
|
322
|
+
export function codexConfigProblem({ message, notAsking, release }) {
|
|
323
|
+
if (!notAsking || notAsking.length === 0) return message;
|
|
324
|
+
const one = notAsking.length === 1;
|
|
325
|
+
const block = release ? `the block hiveku-sync ${release} wrote there` : 'the hiveku-sync block there';
|
|
326
|
+
return (
|
|
327
|
+
`${message} Until it is updated, Codex runs ${toolNames(notAsking)} in this folder without asking: ` +
|
|
328
|
+
`${block} has no prompt for ${one ? 'it' : 'them'}.`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Up to `max` tool names, then how many more. */
|
|
333
|
+
export function toolNames(names, max = 10) {
|
|
334
|
+
if (names.length <= max) return names.join(', ');
|
|
335
|
+
return `${names.slice(0, max).join(', ')} and ${names.length - max} more listed tools`;
|
|
292
336
|
}
|