@hiveku-apps/sync 0.5.13 → 0.5.17
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 +7 -3
- package/bin/hiveku-sync.mjs +7 -1
- package/package.json +1 -1
- package/src/codex-prompt-tools.json +17 -1
- package/src/mcp.mjs +138 -3
- package/src/push.mjs +15 -2
- package/src/saveFiles.mjs +19 -4
package/README.md
CHANGED
|
@@ -185,9 +185,13 @@ them, to add the prompts.
|
|
|
185
185
|
|
|
186
186
|
> **This vs. the plugin.** For a single account, you can instead install the
|
|
187
187
|
> 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`.
|
|
188
|
+
> once (`codex plugin marketplace add locus2012/hiveku-codex-plugin`, then
|
|
189
|
+
> `codex plugin add hiveku@hiveku`, then `codex plugin add hiveku-hooks@hiveku`)
|
|
190
|
+
> and set `HIVEKU_TOKEN`. Already have Hiveku in Codex? Run
|
|
191
|
+
> `codex plugin marketplace upgrade hiveku` before these commands. The first
|
|
192
|
+
> time, Codex asks you to approve Hiveku's hooks: type `/hooks` in Codex and
|
|
193
|
+
> trust them.
|
|
194
|
+
> `hiveku-sync --codex` is the self-contained, per-account route:
|
|
191
195
|
> better for multi-account / CI / power-user setups, and it also brings the
|
|
192
196
|
> account's own knowledge. Use one route per folder, not both.
|
|
193
197
|
|
package/bin/hiveku-sync.mjs
CHANGED
|
@@ -96,7 +96,13 @@ Memory/rules/skills:
|
|
|
96
96
|
Hiveku Codex plugin (single-account users can
|
|
97
97
|
instead run: codex plugin marketplace add
|
|
98
98
|
locus2012/hiveku-codex-plugin, then:
|
|
99
|
-
codex plugin add hiveku@hiveku
|
|
99
|
+
codex plugin add hiveku@hiveku, then:
|
|
100
|
+
codex plugin add hiveku-hooks@hiveku.
|
|
101
|
+
Already have Hiveku in Codex? Run
|
|
102
|
+
codex plugin marketplace upgrade hiveku
|
|
103
|
+
before these commands. The first time,
|
|
104
|
+
Codex asks you to approve Hiveku's hooks:
|
|
105
|
+
type /hooks in Codex and trust them).
|
|
100
106
|
|
|
101
107
|
Coder projects:
|
|
102
108
|
projects List the account's coder projects.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hiveku-apps/sync",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.17",
|
|
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.44 (859caff, on main f588ca8), 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",
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"project_secrets_apply_to_preview",
|
|
28
28
|
"project_secrets_reveal",
|
|
29
29
|
"email_message_get",
|
|
30
|
+
"helpdesk_ticket_attachment_links",
|
|
31
|
+
"helpdesk_contact_data_export",
|
|
30
32
|
"project_secrets_replace",
|
|
31
33
|
"project_secrets_mark_sensitive",
|
|
32
34
|
"env_group_create",
|
|
@@ -54,6 +56,7 @@
|
|
|
54
56
|
"project_indexing_set",
|
|
55
57
|
"project_concurrency_set",
|
|
56
58
|
"project_page_cache_set",
|
|
59
|
+
"analytics_visitor_intelligence_start",
|
|
57
60
|
"ppc_recent_change_override",
|
|
58
61
|
"account_timezone_set",
|
|
59
62
|
"account_memory_append",
|
|
@@ -77,6 +80,15 @@
|
|
|
77
80
|
"crm_estimate_send",
|
|
78
81
|
"helpdesk_ticket_send_reply",
|
|
79
82
|
"helpdesk_assistant_knowledge_sources_update",
|
|
83
|
+
"helpdesk_contact_data_erase",
|
|
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",
|
|
80
92
|
"marketing_report_send",
|
|
81
93
|
"ppc_budget_update",
|
|
82
94
|
"ppc_platform_budget_update",
|
|
@@ -149,6 +161,8 @@
|
|
|
149
161
|
"outbound_campaign_status_set",
|
|
150
162
|
"outbound_campaign_sequences_save",
|
|
151
163
|
"outbound_reply_draft_send",
|
|
164
|
+
"outbound_lead_list_run_start",
|
|
165
|
+
"outbound_lead_list_run_control",
|
|
152
166
|
"voice_number_purchase",
|
|
153
167
|
"voice_number_release",
|
|
154
168
|
"voice_e911_address_create",
|
|
@@ -170,6 +184,7 @@
|
|
|
170
184
|
"voice_sms_toll_free_verification_submit",
|
|
171
185
|
"voice_port_order_create",
|
|
172
186
|
"voice_port_order_action",
|
|
187
|
+
"voice_port_order_document_upload",
|
|
173
188
|
"voice_settings_update",
|
|
174
189
|
"voice_number_update",
|
|
175
190
|
"voice_blocked_numbers_remove",
|
|
@@ -260,6 +275,7 @@
|
|
|
260
275
|
"generate_image_set",
|
|
261
276
|
"media_upscale",
|
|
262
277
|
"marketing_testimonial_media_replace",
|
|
278
|
+
"marketing_testimonial_rerecord_send",
|
|
263
279
|
"marketing_form_upload_settings_update",
|
|
264
280
|
"marketing_form_capture_settings_update",
|
|
265
281
|
"marketing_form_capture_purge",
|
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/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
|
}
|