@hiveku-apps/sync 0.5.14 → 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 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` then
189
- > `codex plugin add hiveku@hiveku`) and set
190
- > `HIVEKU_TOKEN`. `hiveku-sync --codex` is the self-contained, per-account route —
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
 
@@ -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.14",
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.41 (3974fee, on main 616e72b), plugins/hiveku/.mcp.json: every hiveku.tools entry with approval_mode \"prompt\" (project_vcs_rollback included)",
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",
@@ -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,7 @@
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",
180
188
  "voice_settings_update",
181
189
  "voice_number_update",
182
190
  "voice_blocked_numbers_remove",
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: JSON.stringify({ jsonrpc: '2.0', id, method, params }),
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
- throw new Error(`MCP HTTP ${res.status}: ${text.slice(0, 500)}`);
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) failedPaths.push(f.path);
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 throws for
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
  }