@hiveku-apps/sync 0.5.14 → 0.5.18
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 +25 -7
- package/bin/hiveku-sync.mjs +37 -4
- package/package.json +1 -1
- package/src/codex-prompt-tools.json +10 -1
- package/src/codex.mjs +186 -17
- package/src/init.mjs +6 -8
- package/src/mcp.mjs +138 -3
- package/src/pull.mjs +45 -1
- package/src/push.mjs +15 -2
- package/src/saveFiles.mjs +19 -4
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 328 names in `src/codex-prompt-tools.json`, the same list as the
|
|
148
|
+
Hiveku Codex plugin 0.3.46. 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,13 +195,17 @@ 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)
|
|
188
|
-
> once (`codex plugin marketplace add locus2012/hiveku-codex-plugin
|
|
189
|
-
> `codex plugin add hiveku@hiveku`
|
|
190
|
-
> `HIVEKU_TOKEN`.
|
|
202
|
+
> once (`codex plugin marketplace add locus2012/hiveku-codex-plugin`, then
|
|
203
|
+
> `codex plugin add hiveku@hiveku`, then `codex plugin add hiveku-hooks@hiveku`)
|
|
204
|
+
> and set `HIVEKU_TOKEN`. Already have Hiveku in Codex? Run
|
|
205
|
+
> `codex plugin marketplace upgrade hiveku` before these commands. The first
|
|
206
|
+
> time, Codex asks you to approve Hiveku's hooks: type `/hooks` in Codex and
|
|
207
|
+
> trust them.
|
|
208
|
+
> `hiveku-sync --codex` is the self-contained, per-account route:
|
|
191
209
|
> better for multi-account / CI / power-user setups, and it also brings the
|
|
192
210
|
> account's own knowledge. Use one route per folder, not both.
|
|
193
211
|
|
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
|
|
@@ -96,7 +98,13 @@ Memory/rules/skills:
|
|
|
96
98
|
Hiveku Codex plugin (single-account users can
|
|
97
99
|
instead run: codex plugin marketplace add
|
|
98
100
|
locus2012/hiveku-codex-plugin, then:
|
|
99
|
-
codex plugin add hiveku@hiveku
|
|
101
|
+
codex plugin add hiveku@hiveku, then:
|
|
102
|
+
codex plugin add hiveku-hooks@hiveku.
|
|
103
|
+
Already have Hiveku in Codex? Run
|
|
104
|
+
codex plugin marketplace upgrade hiveku
|
|
105
|
+
before these commands. The first time,
|
|
106
|
+
Codex asks you to approve Hiveku's hooks:
|
|
107
|
+
type /hooks in Codex and trust them).
|
|
100
108
|
|
|
101
109
|
Coder projects:
|
|
102
110
|
projects List the account's coder projects.
|
|
@@ -187,6 +195,30 @@ function userToolTablesLines({ userToolTables = [], userTablesWithoutApprovalMod
|
|
|
187
195
|
return lines;
|
|
188
196
|
}
|
|
189
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
|
+
|
|
190
222
|
/**
|
|
191
223
|
* What refreshing CLAUDE.md's kept-current part (folder layout and memory
|
|
192
224
|
* rules, claude-md.mjs) did, as lines to print. Nothing when it was already
|
|
@@ -333,6 +365,7 @@ async function main() {
|
|
|
333
365
|
console.log('They were left on disk. Delete them manually if no longer needed.');
|
|
334
366
|
}
|
|
335
367
|
for (const line of claudeMdLines(summary.claude_md)) console.log(line);
|
|
368
|
+
for (const line of codexConfigLines(summary.codex_config)) console.log(line);
|
|
336
369
|
if (summary.codex_lane) {
|
|
337
370
|
console.log('\nCodex: run `hiveku-sync codex` to refresh the rules list in AGENTS.md and the skills in .agents/skills/.');
|
|
338
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.18",
|
|
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 feature/ask-loa-send (0.3.46, on main d667b1b), 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",
|
|
@@ -82,6 +82,13 @@
|
|
|
82
82
|
"helpdesk_assistant_knowledge_sources_update",
|
|
83
83
|
"helpdesk_contact_data_erase",
|
|
84
84
|
"helpdesk_chat_retention_set",
|
|
85
|
+
"helpdesk_social_comments_settings_update",
|
|
86
|
+
"voice_site_create",
|
|
87
|
+
"voice_role_assign",
|
|
88
|
+
"voice_role_remove",
|
|
89
|
+
"voice_api_key_create",
|
|
90
|
+
"voice_api_key_revoke",
|
|
91
|
+
"voice_desk_phone_set",
|
|
85
92
|
"marketing_report_send",
|
|
86
93
|
"ppc_budget_update",
|
|
87
94
|
"ppc_platform_budget_update",
|
|
@@ -177,6 +184,8 @@
|
|
|
177
184
|
"voice_sms_toll_free_verification_submit",
|
|
178
185
|
"voice_port_order_create",
|
|
179
186
|
"voice_port_order_action",
|
|
187
|
+
"voice_port_order_document_upload",
|
|
188
|
+
"voice_port_order_loa_send",
|
|
180
189
|
"voice_settings_update",
|
|
181
190
|
"voice_number_update",
|
|
182
191
|
"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/mcp.mjs
CHANGED
|
@@ -1,10 +1,138 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Minimal MCP JSON-RPC client over Streamable HTTP.
|
|
3
3
|
* Speaks just enough of the protocol to initialize + call tools.
|
|
4
|
+
*
|
|
5
|
+
* Every body goes gzip-compressed, and a refusal from the web edge in front of
|
|
6
|
+
* Hiveku fails only the request it refused (HK-847, below). This is the only
|
|
7
|
+
* place hiveku-sync posts to core.hiveku.com; it posts to nothing else.
|
|
4
8
|
*/
|
|
9
|
+
import { gzipSync } from 'node:zlib';
|
|
5
10
|
|
|
6
11
|
const PROTOCOL_VERSION = '2024-11-05';
|
|
7
12
|
|
|
13
|
+
/**
|
|
14
|
+
* ★ EVERY BODY TO CORE GOES GZIP-COMPRESSED (HK-847): Content-Encoding gzip,
|
|
15
|
+
* the Content-Type unchanged, and the JSON inside byte for byte what it was.
|
|
16
|
+
*
|
|
17
|
+
* The web edge in front of core.hiveku.com reads plain request bodies against
|
|
18
|
+
* content rules nobody at Hiveku can see or change. On 2026-10-28 it answered a
|
|
19
|
+
* pm_tasks_create from the Claude Code plugin with an HTML "Blocked" page
|
|
20
|
+
* because of a file path in the task's text (HK-838), and the same body sent
|
|
21
|
+
* gzip-compressed passed. A push carries whole files, code and file paths
|
|
22
|
+
* included, so this client needs it as much as the plugin (lib/upstream.mjs
|
|
23
|
+
* gzipBody, since 0.27.111) and the VS Code extension (src/mcpClient.ts).
|
|
24
|
+
*
|
|
25
|
+
* WHERE THE SERVER INFLATES IT (hiveku-mcp-api-server, checked 2026-10-29):
|
|
26
|
+
* src/middleware/json-body.ts is its one JSON body parser, mounted app-wide in
|
|
27
|
+
* src/index-unified-http.ts before every route. It inflates gzip and deflate
|
|
28
|
+
* (body-parser's default) and counts its 30 MB limit after inflating, so a
|
|
29
|
+
* compressed body has the ceiling a plain one had.
|
|
30
|
+
*/
|
|
31
|
+
export function gzipJsonBody(message) {
|
|
32
|
+
return gzipSync(Buffer.from(JSON.stringify(message), 'utf8'));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* ★ WHO REFUSED: HIVEKU, OR THE WEB EDGE IN FRONT OF IT (HK-847).
|
|
37
|
+
*
|
|
38
|
+
* 401, 403 and 429 come from both. Hiveku's MCP server sends each one as a
|
|
39
|
+
* JSON-RPC error object: mcp-auth.ts for a revoked key (401) or a key used for
|
|
40
|
+
* another account (403), rate-limiter.ts for its 429. The edge sends a page of
|
|
41
|
+
* its own instead, and the request never reached Hiveku. So the BODY decides,
|
|
42
|
+
* never the status alone: Hiveku's own JSON-RPC error keeps the `MCP HTTP
|
|
43
|
+
* <status>: <body>` it always had, and anything else on those statuses (an
|
|
44
|
+
* HTML page, a "Blocked" title, plain text, an empty body, JSON that is not a
|
|
45
|
+
* JSON-RPC error) is the edge refusing THIS request: McpEdgeRefusalError, a
|
|
46
|
+
* plain message with the edge's ray id that never calls it a key problem.
|
|
47
|
+
* 503 is not one of them: the proxy in front of Hiveku also answers 503 when a
|
|
48
|
+
* deploy cuts a call it had already delivered, so it keeps the generic path.
|
|
49
|
+
* The words are exactly the VS Code extension's (src/mcpClient.ts
|
|
50
|
+
* coreEdgeRefusalMessage); the plugin (lib/upstream.mjs edgeRefusalError)
|
|
51
|
+
* says the same facts to a model, with the next steps a model has.
|
|
52
|
+
*/
|
|
53
|
+
const EDGE_REFUSAL_STATUSES = new Set([401, 403, 429]);
|
|
54
|
+
|
|
55
|
+
/** True for Hiveku's own error answer: JSON-RPC 2.0 whose `error` has a whole-number code and a string message. */
|
|
56
|
+
export function isHivekuErrorAnswer(text) {
|
|
57
|
+
if (typeof text !== 'string' || !text.trim()) return false;
|
|
58
|
+
let body;
|
|
59
|
+
try {
|
|
60
|
+
body = JSON.parse(text);
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (!body || typeof body !== 'object' || Array.isArray(body) || body.jsonrpc !== '2.0') return false;
|
|
65
|
+
const { error } = body;
|
|
66
|
+
if (!error || typeof error !== 'object' || Array.isArray(error)) return false;
|
|
67
|
+
return Number.isInteger(error.code) && typeof error.message === 'string';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A ray id is an id: only an id-shaped value is passed on. */
|
|
71
|
+
const RAY_HEADER_RE = /^[0-9a-f]{16}(?:-[a-z]{3,4})?$/i;
|
|
72
|
+
/** "Ray ID: <strong>8f1c...</strong>" in a block page, or "ray_id":"8f1c..." in a JSON one. */
|
|
73
|
+
const RAY_IN_PAGE_RE = /ray[\s_-]*id[\s\S]{0,80}?(?<![0-9a-z])([0-9a-f]{16})(?![0-9a-z])/i;
|
|
74
|
+
|
|
75
|
+
/** The ray id the edge gave its refusal: the cf-ray header first, else one named in the page. null when neither holds one. */
|
|
76
|
+
export function edgeRayId(headers, text) {
|
|
77
|
+
const header = String(headers?.get?.('cf-ray') ?? '').trim();
|
|
78
|
+
if (RAY_HEADER_RE.test(header)) return header;
|
|
79
|
+
const found = typeof text === 'string' ? text.slice(0, 64 * 1024).match(RAY_IN_PAGE_RE) : null;
|
|
80
|
+
return found ? found[1] : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The words for a refusal from the web edge in front of Hiveku. They claim only
|
|
85
|
+
* what this client knows: the edge refused this request, so it never reached
|
|
86
|
+
* Hiveku, and Hiveku did not refuse the key. Not why: a rule on a request's text
|
|
87
|
+
* is the usual cause, but a rule on where requests come from answers every
|
|
88
|
+
* request the same way, so a 401 or 403 says what to do then. The ray id comes
|
|
89
|
+
* first, so a message cut short still carries it. Plain words: no vendor names.
|
|
90
|
+
*/
|
|
91
|
+
export function edgeRefusalMessage(status, rayId) {
|
|
92
|
+
const byText = status === 401 || status === 403;
|
|
93
|
+
const label = rayId ? `HTTP ${status}, ray id ${rayId}` : `HTTP ${status}`;
|
|
94
|
+
const give = rayId ? `ray id ${rayId}` : 'the time it happened';
|
|
95
|
+
const what = byText
|
|
96
|
+
? `The web edge in front of Hiveku refused this one request (${label}), most often because of something in its text.`
|
|
97
|
+
: `The web edge in front of Hiveku turned this one request away (${label}). It does that when it is busy, and also ` +
|
|
98
|
+
"because of something in a request's text.";
|
|
99
|
+
const next = byText
|
|
100
|
+
? 'Try it again with the text shortened or rephrased: code and file paths are the usual cause. If every request ' +
|
|
101
|
+
'is refused this way, even a plain one, the block is on where the requests come from, not on their text: tell ' +
|
|
102
|
+
`Hiveku support and give them ${give}.`
|
|
103
|
+
: 'Wait a minute and try it once more. If it is turned away again, shorten or rephrase the text: code and file ' +
|
|
104
|
+
`paths are the usual cause. If it keeps happening, tell Hiveku support and give them ${give}.`;
|
|
105
|
+
return (
|
|
106
|
+
`${what} It never reached Hiveku and nothing was saved. Hiveku did not refuse your key, so there is no need to ` +
|
|
107
|
+
`reconnect: the next request is sent as usual. ${next}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The words for a 401, 403 or 429 whose answer could not be read, so who refused it is not known. */
|
|
112
|
+
export function refusalUnreadMessage(status) {
|
|
113
|
+
return (
|
|
114
|
+
`The answer to this request (HTTP ${status}) could not be read, so it is not known whether Hiveku or ` +
|
|
115
|
+
'the web edge in front of Hiveku refused it. It is not taken as a sign that your key stopped working: the next ' +
|
|
116
|
+
'request is sent as usual. Try it once more.'
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* A 401, 403 or 429 that was not Hiveku's own JSON-RPC error. `edge_blocked`:
|
|
122
|
+
* the edge's own page, so the request never reached Hiveku. `unreadable`: the
|
|
123
|
+
* answer could not be read, so who refused it is not known. Neither is a key
|
|
124
|
+
* problem.
|
|
125
|
+
*/
|
|
126
|
+
export class McpEdgeRefusalError extends Error {
|
|
127
|
+
constructor(kind, httpStatus, rayId = null) {
|
|
128
|
+
super(kind === 'edge_blocked' ? edgeRefusalMessage(httpStatus, rayId) : refusalUnreadMessage(httpStatus));
|
|
129
|
+
this.name = 'McpEdgeRefusalError';
|
|
130
|
+
this.kind = kind;
|
|
131
|
+
this.httpStatus = httpStatus;
|
|
132
|
+
this.rayId = kind === 'edge_blocked' ? rayId : null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
8
136
|
export class HivekuMcpClient {
|
|
9
137
|
constructor({ baseUrl, apiKey, profile = 'full' }) {
|
|
10
138
|
if (!baseUrl) throw new Error('baseUrl required');
|
|
@@ -30,6 +158,8 @@ export class HivekuMcpClient {
|
|
|
30
158
|
const headers = {
|
|
31
159
|
Authorization: `Bearer ${this.apiKey}`,
|
|
32
160
|
'Content-Type': 'application/json',
|
|
161
|
+
// The body is gzip, so the edge's content rules never read it (gzipJsonBody).
|
|
162
|
+
'Content-Encoding': 'gzip',
|
|
33
163
|
Accept: 'application/json',
|
|
34
164
|
};
|
|
35
165
|
if (this.sessionId) headers['Mcp-Session-Id'] = this.sessionId;
|
|
@@ -37,15 +167,20 @@ export class HivekuMcpClient {
|
|
|
37
167
|
const res = await fetch(this.endpoint, {
|
|
38
168
|
method: 'POST',
|
|
39
169
|
headers,
|
|
40
|
-
body:
|
|
170
|
+
body: gzipJsonBody({ jsonrpc: '2.0', id, method, params }),
|
|
41
171
|
});
|
|
42
172
|
|
|
43
173
|
const sessionHeader = res.headers.get('mcp-session-id');
|
|
44
174
|
if (sessionHeader) this.sessionId = sessionHeader;
|
|
45
175
|
|
|
46
176
|
if (!res.ok) {
|
|
47
|
-
const text = await res.text().catch(() =>
|
|
48
|
-
|
|
177
|
+
const text = await res.text().catch(() => null);
|
|
178
|
+
// ★ A 401, 403 or 429 is classified by its body first (EDGE_REFUSAL_STATUSES).
|
|
179
|
+
if (EDGE_REFUSAL_STATUSES.has(res.status)) {
|
|
180
|
+
if (text === null) throw new McpEdgeRefusalError('unreadable', res.status);
|
|
181
|
+
if (!isHivekuErrorAnswer(text)) throw new McpEdgeRefusalError('edge_blocked', res.status, edgeRayId(res.headers, text));
|
|
182
|
+
}
|
|
183
|
+
throw new Error(`MCP HTTP ${res.status}: ${(text ?? '').slice(0, 500)}`);
|
|
49
184
|
}
|
|
50
185
|
|
|
51
186
|
if (res.status === 204) return null;
|
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
|
}
|
package/src/push.mjs
CHANGED
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
tryReadVersionStatus,
|
|
70
70
|
versionAfterPush,
|
|
71
71
|
} from './versions.mjs';
|
|
72
|
+
import { McpEdgeRefusalError } from './mcp.mjs';
|
|
72
73
|
import { saveCodeFiles } from './saveFiles.mjs';
|
|
73
74
|
import {
|
|
74
75
|
branchMovedSafeNote,
|
|
@@ -423,6 +424,11 @@ export async function pushProject({
|
|
|
423
424
|
log: (line) => console.log(line),
|
|
424
425
|
});
|
|
425
426
|
const failedPaths = [...code.failed];
|
|
427
|
+
// The words of the last request the web edge in front of Hiveku refused
|
|
428
|
+
// (mcp.mjs McpEdgeRefusalError, HK-847), printed with the files it kept from
|
|
429
|
+
// saving so the person sees why, and the edge's ray id. Set only where that
|
|
430
|
+
// refusal left a file unsaved, never for one a retry got past.
|
|
431
|
+
let edgeRefusal = code.refusal ?? null;
|
|
426
432
|
const codeSaved = code.saved;
|
|
427
433
|
// The branch tree this push leaves behind (every branch write answers it).
|
|
428
434
|
let branchEtag = code.workingTreeEtag;
|
|
@@ -444,7 +450,12 @@ export async function pushProject({
|
|
|
444
450
|
}
|
|
445
451
|
return;
|
|
446
452
|
} catch (e) {
|
|
447
|
-
if (attempt >= 2)
|
|
453
|
+
if (attempt >= 2) {
|
|
454
|
+
failedPaths.push(f.path);
|
|
455
|
+
// Only the attempt that left the file unsaved says why: an edge
|
|
456
|
+
// refusal that a second attempt got past says nothing (HK-847).
|
|
457
|
+
if (e instanceof McpEdgeRefusalError) edgeRefusal = e.message;
|
|
458
|
+
}
|
|
448
459
|
}
|
|
449
460
|
}
|
|
450
461
|
});
|
|
@@ -461,7 +472,8 @@ export async function pushProject({
|
|
|
461
472
|
const etag = answer?.working_tree_etag ?? answer?.data?.working_tree_etag;
|
|
462
473
|
if (typeof etag === 'string' && etag) branchEtag = etag;
|
|
463
474
|
deleted += 1;
|
|
464
|
-
} catch {
|
|
475
|
+
} catch (e) {
|
|
476
|
+
if (e instanceof McpEdgeRefusalError) edgeRefusal = e.message;
|
|
465
477
|
failedPaths.push(p);
|
|
466
478
|
}
|
|
467
479
|
}
|
|
@@ -478,6 +490,7 @@ export async function pushProject({
|
|
|
478
490
|
` ${failedPaths.length} file${failedPaths.length !== 1 ? 's' : ''} did NOT save: ` +
|
|
479
491
|
`${failedPaths.slice(0, 10).join(', ')}${failedPaths.length > 10 ? '…' : ''}`,
|
|
480
492
|
);
|
|
493
|
+
if (edgeRefusal) console.log(` ${edgeRefusal}`);
|
|
481
494
|
console.log(' Every other file IS saved. Push again to retry the ones that failed.');
|
|
482
495
|
}
|
|
483
496
|
|
package/src/saveFiles.mjs
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* Builder contract: hiveku_builder feature/versions-core
|
|
18
18
|
* src/app/api/olympus/builder/projects/[projectId]/files-bulk/route.ts (POST).
|
|
19
19
|
*/
|
|
20
|
+
import { McpEdgeRefusalError } from './mcp.mjs';
|
|
20
21
|
|
|
21
22
|
/** Batches of about 25 text files finish inside the route's time budget (the tool's own advice). */
|
|
22
23
|
export const BATCH_FILES = 25;
|
|
@@ -71,16 +72,24 @@ export function readSaveAnswer(data, sentPaths) {
|
|
|
71
72
|
|
|
72
73
|
/**
|
|
73
74
|
* Save `files` ({ path, content, encoding }) to Your site (branch null) or a
|
|
74
|
-
* branch. Returns { saved, failed: [paths], workingTreeEtag }. Never
|
|
75
|
-
* a refused or failed call: those files are reported in `failed`.
|
|
75
|
+
* branch. Returns { saved, failed: [paths], workingTreeEtag, refusal }. Never
|
|
76
|
+
* throws for a refused or failed call: those files are reported in `failed`.
|
|
77
|
+
* `refusal` is the web edge's words (mcp.mjs McpEdgeRefusalError, HK-847),
|
|
78
|
+
* with its ray id, when the edge refused the last attempt of a batch whose
|
|
79
|
+
* files are in `failed`, so the person is told why those did not save; else
|
|
80
|
+
* null. A refused attempt followed by one Hiveku answered says nothing about
|
|
81
|
+
* the batch any more, so its words are dropped.
|
|
76
82
|
*/
|
|
77
83
|
export async function saveCodeFiles({ client, projectId, files, branch = null, note, log = () => {} }) {
|
|
78
84
|
const savedPaths = new Set();
|
|
79
85
|
const failed = [];
|
|
80
86
|
let workingTreeEtag = null;
|
|
87
|
+
let refusal = null;
|
|
81
88
|
const total = files.length;
|
|
82
89
|
for (const batch of batchFiles(files)) {
|
|
83
90
|
let pending = batch;
|
|
91
|
+
// The edge's words when it refused this batch's latest attempt, else null.
|
|
92
|
+
let batchRefusal = null;
|
|
84
93
|
for (let attempt = 1; attempt <= ATTEMPTS && pending.length; attempt++) {
|
|
85
94
|
const sentPaths = pending.map((f) => f.path);
|
|
86
95
|
let data;
|
|
@@ -94,10 +103,14 @@ export async function saveCodeFiles({ client, projectId, files, branch = null, n
|
|
|
94
103
|
...(branch ? { branch } : {}),
|
|
95
104
|
})
|
|
96
105
|
)?.data;
|
|
97
|
-
} catch {
|
|
106
|
+
} catch (err) {
|
|
98
107
|
// No answer: nothing is known about this batch, so all of it is sent again.
|
|
108
|
+
// The edge refusing it never reached Hiveku; its words are kept for the person.
|
|
109
|
+
batchRefusal = err instanceof McpEdgeRefusalError ? err.message : null;
|
|
99
110
|
continue;
|
|
100
111
|
}
|
|
112
|
+
// Hiveku answered, so it, not the edge, says what did not land.
|
|
113
|
+
batchRefusal = null;
|
|
101
114
|
const answer = readSaveAnswer(data, sentPaths);
|
|
102
115
|
for (const p of answer.landed) savedPaths.add(p);
|
|
103
116
|
if (answer.etag) workingTreeEtag = answer.etag;
|
|
@@ -105,7 +118,9 @@ export async function saveCodeFiles({ client, projectId, files, branch = null, n
|
|
|
105
118
|
pending = pending.filter((f) => again.has(f.path) && !savedPaths.has(f.path));
|
|
106
119
|
}
|
|
107
120
|
failed.push(...pending.map((f) => f.path));
|
|
121
|
+
// Set only by a refused last attempt, which leaves this batch's files in `failed`.
|
|
122
|
+
if (batchRefusal) refusal = batchRefusal;
|
|
108
123
|
log(` code ${savedPaths.size}/${total}`);
|
|
109
124
|
}
|
|
110
|
-
return { saved: savedPaths.size, failed, workingTreeEtag };
|
|
125
|
+
return { saved: savedPaths.size, failed, workingTreeEtag, refusal };
|
|
111
126
|
}
|