@hiveku-apps/sync 0.1.0 → 0.3.0

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
@@ -78,6 +78,92 @@ narrowed tool list. Available: `full`, `sales`, `marketing`, `helpdesk`,
78
78
  `pm`, `dev`. Change `HIVEKU_PROFILE` in `.env` and reload the MCP server
79
79
  in your editor to switch.
80
80
 
81
+ ## Codex (OpenAI) support
82
+
83
+ By default `init` scaffolds a Claude Code folder (`.mcp.json` + `CLAUDE.md`). Add
84
+ `--codex` to also write the Codex-native equivalents so the same folder works
85
+ from the **Codex CLI / IDE extension / ChatGPT desktop**:
86
+
87
+ ```bash
88
+ # Scaffold with the Codex lane too:
89
+ hiveku-sync init acme-corp --codex
90
+
91
+ # Or retrofit an existing account folder (run after `pull` to include the
92
+ # account's latest skills/commands):
93
+ cd acme-corp && hiveku-sync codex
94
+ ```
95
+
96
+ This writes, merge-safely (only Hiveku-managed regions are touched):
97
+
98
+ - **`AGENTS.md`** — Codex's `CLAUDE.md` equivalent: a lean pointer to `CLAUDE.md`
99
+ plus the load-bearing rules (Codex caps the instruction chain at ~32 KiB).
100
+ - **`.codex/config.toml`** — the same `core.hiveku.com/mcp` server via
101
+ `[mcp_servers.hiveku]`, git-ignored. Your `OLYMPUS_API_KEY` is **inlined** when
102
+ `.env` has it (self-contained, no env setup); otherwise it's read from the
103
+ `OLYMPUS_API_KEY` env var. Also wires `playwright` for visual checks.
104
+ - **`.agents/skills/<name>/SKILL.md`** — the account's pulled `skills/` and
105
+ `commands/` mirrored as repo-scoped Codex skills (Codex prompts are global-only
106
+ and deprecated).
107
+
108
+ Pass `--trust` to pre-trust the folder in `~/.codex/config.toml` (so Codex loads
109
+ the repo config without its first-run trust prompt). Codex ignores repo-level
110
+ approval/sandbox config, so the safe-work rules ship as instructions in
111
+ `AGENTS.md`, not enforced permissions.
112
+
113
+ > **This vs. the plugin.** For a single account, you can instead install the
114
+ > published [Hiveku Codex plugin](https://github.com/locus2012/hiveku-codex-plugin)
115
+ > once (`codex plugin marketplace add locus2012/hiveku-codex-plugin`) and set
116
+ > `HIVEKU_TOKEN`. `hiveku-sync --codex` is the self-contained, per-account route —
117
+ > better for multi-account / CI / power-user setups, and it also brings the
118
+ > account's own knowledge. Use one route per folder, not both.
119
+
120
+ ## Coder projects — pull, ship, deploy
121
+
122
+ Beyond memory/rules/skills, `hiveku-sync` can drive a Hiveku **website project**
123
+ end-to-end from the terminal — the same push/deploy loop the VS Code extension's
124
+ Source Control gives you, but headless (Claude Code in a terminal, CI, a cron).
125
+
126
+ ```bash
127
+ hiveku-sync projects # list the account's coder projects
128
+ hiveku-sync pull-project <slug-or-id> # → ./projects/<slug>/ (+ .hiveku/projects.json)
129
+
130
+ cd projects/<slug>
131
+ hiveku-sync pull-env # secrets → .env.local (git-ignored)
132
+ # ...edit files, run the dev server...
133
+ hiveku-sync status # diff local vs the deploy source
134
+ hiveku-sync push # save changes to the deploy source
135
+ hiveku-sync push --deploy development --wait # ...then deploy and wait
136
+ ```
137
+
138
+ **`push` writes the deploy source, lane-routed.** This is the important part.
139
+ `deploy_site` builds from `builder_code_versions` (+ the S3/CDN asset store) —
140
+ **not** a GitHub branch. So `push`:
141
+
142
+ - routes **CDN-servable binary** (images/fonts/video under `public/<subdir>/`)
143
+ to `assets_upload` (S3 → CloudFront). A binary put in `builder_code_versions`
144
+ renders in the Fly preview but **vanishes on deploy** (the build excludes it) —
145
+ `push` never makes that mistake.
146
+ - routes everything else (code, config, text-under-public) to
147
+ `project_files_bulk_save` in small, per-batch-verified batches (transport-safe
148
+ vs one giant call).
149
+ - diffs correctly: code via a single `project_files_status` call (so
150
+ `only_remote` stays reliable — chunking it produces phantom deletions), assets
151
+ via `assets_list` (so unchanged images aren't re-uploaded every push).
152
+
153
+ Flags: `--include-deletes` (remove code paths missing locally), `--no-assets` /
154
+ `--assets-all`, `--commit "msg"` (also record a Hiveku-native `main` commit),
155
+ `--deploy <env> [--wait]`, `--dry-run`.
156
+
157
+ ```bash
158
+ hiveku-sync deploy <development|staging|production> [<slug>] [--wait]
159
+ hiveku-sync pull-env [<slug>] [--target development|staging|production]
160
+ hiveku-sync commit -m "msg" # legacy GitHub-branch commit (GitHub-connected projects only)
161
+ ```
162
+
163
+ `pull-env` applies the same `_DEV` / `_PROD` / `_STAGING` suffix resolution the
164
+ preview machine uses: for `development` it strips `_DEV` to the base key and
165
+ skips `_PROD`/`_STAGING`; `--target production` does the inverse.
166
+
81
167
  ## Safety
82
168
 
83
169
  - **Manifest pin** — `.hiveku/manifest.json` records the API key preview.
@@ -91,7 +177,7 @@ in your editor to switch.
91
177
 
92
178
  ## How writes work
93
179
 
94
- `hiveku-sync` is pull-only. To persist changes back to Olympus, call the
95
- `memory_create` / `memory_update` / `memory_delete` MCP tools through
96
- Claude (or whichever LLM you've wired up). Then `hiveku-sync pull` to
97
- refresh the local copies.
180
+ For **projects**, `hiveku-sync push` / `deploy` write straight back to Hiveku
181
+ (see "Coder projects" above). For **memory/rules/skills**, `pull` is read-only —
182
+ persist changes with the `memory_create` / `memory_update` / `memory_delete` MCP
183
+ tools through Claude, then `hiveku-sync pull` to refresh the local copies.
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { runInit } from '../src/init.mjs';
12
+ import { runCodex } from '../src/init.mjs';
12
13
  import { runPull } from '../src/pull.mjs';
13
14
  import {
14
15
  runProjectsList,
@@ -16,20 +17,39 @@ import {
16
17
  runProjectStatus,
17
18
  runProjectPush,
18
19
  } from '../src/projects.mjs';
20
+ import { runPush, runDeploy, runPullEnv } from '../src/push.mjs';
21
+ import { pathToFileURL } from 'node:url';
19
22
 
20
- function parseFlags(argv) {
23
+ export function parseFlags(argv) {
21
24
  const positional = [];
22
25
  const flags = {};
23
- // Short flags that take a value (next arg is the value, not a new flag).
26
+ // Short flags that take the NEXT arg as their value.
24
27
  const VALUE_SHORT = new Set(['m']);
28
+ // Long flags that take the next arg as their value when not given with `=`.
29
+ const VALUE_LONG = new Set([
30
+ 'message',
31
+ 'commit',
32
+ 'deploy',
33
+ 'target',
34
+ 'branch',
35
+ 'profile',
36
+ 'parent',
37
+ ]);
25
38
  for (let i = 0; i < argv.length; i++) {
26
39
  const arg = argv[i];
27
40
  if (arg.startsWith('--')) {
28
41
  const eq = arg.indexOf('=');
29
- if (eq === -1) {
30
- flags[arg.slice(2)] = true;
31
- } else {
42
+ if (eq !== -1) {
32
43
  flags[arg.slice(2, eq)] = arg.slice(eq + 1);
44
+ } else {
45
+ const key = arg.slice(2);
46
+ const nextIsValue = i + 1 < argv.length && !argv[i + 1].startsWith('-');
47
+ if (VALUE_LONG.has(key) && nextIsValue) {
48
+ flags[key] = argv[i + 1];
49
+ i += 1;
50
+ } else {
51
+ flags[key] = true;
52
+ }
33
53
  }
34
54
  } else if (arg.startsWith('-') && arg.length > 1) {
35
55
  const key = arg.slice(1);
@@ -50,27 +70,64 @@ const HELP = `hiveku-sync — sync a Hiveku account into a local Claude Code fol
50
70
 
51
71
  Memory/rules/skills:
52
72
  init <slug> [--profile=<name>] Scaffold ./<slug>/ with .env, .mcp.json,
53
- CLAUDE.md, and the memory/rules/skills tree.
73
+ [--codex] [--trust] CLAUDE.md, and the memory/rules/skills tree.
74
+ --codex also writes the Codex lane (AGENTS.md
75
+ + .codex/config.toml + skills); --trust
76
+ pre-trusts the folder in ~/.codex/config.toml.
54
77
  pull [--profile=<name>] Pull memory/rules/skills via MCP into the
55
78
  current account folder. Reads OLYMPUS_API_KEY
56
79
  and OLYMPUS_BASE_URL from ./.env.
80
+ codex [<folder>] [--trust] Retrofit an existing account folder with the
81
+ Codex lane (AGENTS.md + .codex/config.toml +
82
+ mirror the account's skills/commands to
83
+ .agents/skills/). Run after 'pull' to include
84
+ the latest knowledge. Complements the published
85
+ Hiveku Codex plugin (single-account users can
86
+ instead run: codex plugin marketplace add
87
+ locus2012/hiveku-codex-plugin).
57
88
 
58
89
  Coder projects:
59
90
  projects List the account's coder projects.
60
91
  pull-project <slug-or-id> Pull a project's source into ./projects/<slug>/.
61
92
  [--branch=<name>] Records the project mapping in .hiveku/projects.json.
62
- status [<slug>] [--json] Diff local ./projects/<slug>/ vs Hiveku DB.
63
- cd into the project folder to omit <slug>.
64
- push [<slug>] -m "<message>" Save+commit changed files to the project's
65
- [--branch=<name>] GitHub branch via MCP (same path as the v3
66
- [--include-deletes] sidebar). Add --include-deletes to commit
67
- [--dry-run] paths missing locally as deletions.
93
+ status [<slug>] [--json] Diff local ./projects/<slug>/ vs the deploy source
94
+ (builder_code_versions). cd in to omit <slug>.
95
+
96
+ Ship a project (the agent-native deploy loop):
97
+ push [<slug>] Save local changes to the DEPLOY SOURCE, lane-routed:
98
+ [--include-deletes] images/fonts/video under public/<subdir>/ → the
99
+ [--no-assets] [--assets-all] S3/CDN asset store; everything else → batched,
100
+ [--commit "<msg>"] verified bulk_save. This is what deploy reads.
101
+ [--deploy <env>] [--wait] --commit also records a Hiveku 'main' commit.
102
+ [--dry-run] [--branch=<name>] --deploy <env> [--wait] deploys after the push.
103
+ deploy <env> [<slug>] [--wait] Deploy to development | staging | production and
104
+ (with --wait) poll to a terminal status.
105
+ pull-env [<slug>] Write project secrets → .env.local (git-ignored),
106
+ [--target <env>] with the same _DEV/_PROD/_STAGING resolution the
107
+ preview uses. --target staging|production for the
108
+ other tiers.
109
+ commit [<slug>] -m "<msg>" GitHub-branch commit via project_commit (only for
110
+ [--branch=<name>] GitHub-connected projects; most sites use push).
111
+ [--include-deletes]
68
112
 
69
113
  help Show this message.
70
114
 
71
115
  Profiles: full, sales, marketing, helpdesk, pm, dev
72
116
  `;
73
117
 
118
+ function commitMessageFrom(flags) {
119
+ // --commit "msg" | --commit=msg | -m "msg" | --message "msg". A bare --commit
120
+ // (boolean true) means "commit with no message" → fall back to the default.
121
+ const c = typeof flags['commit'] === 'string' ? flags['commit'] : undefined;
122
+ const m = typeof flags['m'] === 'string' ? flags['m'] : undefined;
123
+ const msg = typeof flags['message'] === 'string' ? flags['message'] : undefined;
124
+ if (c) return c;
125
+ if (m) return m;
126
+ if (msg) return msg;
127
+ if (flags['commit'] === true) return 'hiveku-sync: push';
128
+ return undefined;
129
+ }
130
+
74
131
  async function main() {
75
132
  const [cmd, ...rest] = process.argv.slice(2);
76
133
  const { positional, flags } = parseFlags(rest);
@@ -87,6 +144,8 @@ async function main() {
87
144
  slug,
88
145
  parentDir: flags['parent'],
89
146
  profile: flags['profile'] || 'full',
147
+ codex: !!flags['codex'],
148
+ trust: !!flags['trust'],
90
149
  });
91
150
  console.log(`Initialized ${result.folder}`);
92
151
  console.log(` profile: ${result.profile}`);
@@ -96,9 +155,33 @@ async function main() {
96
155
  if (result.workspaceAdded) {
97
156
  console.log(` workspace: added entry to hiveku.code-workspace`);
98
157
  }
158
+ if (result.codex) {
159
+ console.log(` codex: wrote AGENTS.md + .codex/config.toml (git-ignored)`);
160
+ console.log(
161
+ result.codex.inlinedKey
162
+ ? ` MCP auth: inlined your OLYMPUS_API_KEY`
163
+ : ` MCP auth: reads the OLYMPUS_API_KEY env var — after pasting your key into .env, run \`hiveku-sync codex\` to inline it, or \`export OLYMPUS_API_KEY=...\``,
164
+ );
165
+ if (result.codex.trust === 'added') console.log(` trust: pre-trusted this folder in ~/.codex/config.toml`);
166
+ }
99
167
  console.log(` then: cd ${result.folder} && hiveku-sync pull`);
100
168
  break;
101
169
  }
170
+ case 'codex': {
171
+ const result = await runCodex({ folder: positional[0], trust: !!flags['trust'] });
172
+ console.log(`Codex lane ready in ${result.folder} (profile=${result.profile})`);
173
+ console.log(` AGENTS.md + .codex/config.toml written (git-ignored).`);
174
+ console.log(
175
+ result.inlinedKey
176
+ ? ` MCP auth: inlined OLYMPUS_API_KEY from .env.`
177
+ : ` MCP auth: reads the OLYMPUS_API_KEY env var (no key found in .env to inline) — set it, or paste it into .env and re-run.`,
178
+ );
179
+ console.log(` skills: mirrored ${result.skillsMirrored} account skill/command entr${result.skillsMirrored === 1 ? 'y' : 'ies'} into .agents/skills/.`);
180
+ if (result.trust === 'added') console.log(` trust: pre-trusted this folder in ~/.codex/config.toml.`);
181
+ else if (result.trust === 'already') console.log(` trust: folder already trusted.`);
182
+ console.log(` verify: cd there, run \`codex mcp list\` (the hiveku row), then \`get_account_info\`.`);
183
+ break;
184
+ }
102
185
  case 'pull': {
103
186
  const summary = await runPull({ profile: flags['profile'] });
104
187
  console.log(`Pulled ${summary.total} entries (profile=${summary.profile}, base=${summary.baseUrl}):`);
@@ -144,16 +227,55 @@ async function main() {
144
227
  break;
145
228
  }
146
229
  case 'push': {
147
- // Positional layout: `push [<slug>]`. -m / --message is required.
148
- const slug = positional[0];
149
- const message = flags['m'] || flags['message'];
150
- if (!message) {
151
- console.error('Error: push requires -m "<commit message>"\n');
230
+ const deployEnv = typeof flags['deploy'] === 'string' ? flags['deploy'] : undefined;
231
+ await runPush({
232
+ slug: positional[0],
233
+ profile: flags['profile'],
234
+ commit: commitMessageFrom(flags),
235
+ deploy: deployEnv,
236
+ wait: !!flags['wait'],
237
+ includeDeletes: !!flags['include-deletes'],
238
+ noAssets: !!flags['no-assets'],
239
+ assetsAll: !!flags['assets-all'],
240
+ dryRun: !!flags['dry-run'],
241
+ branch: flags['branch'],
242
+ });
243
+ break;
244
+ }
245
+ case 'deploy': {
246
+ // `deploy <env> [<slug>]` — env is the first positional.
247
+ const environment = positional[0];
248
+ if (!environment) {
249
+ console.error('Error: deploy requires an environment: development | staging | production\n');
250
+ console.error(HELP);
251
+ process.exit(1);
252
+ }
253
+ await runDeploy({
254
+ environment,
255
+ slugOrId: positional[1],
256
+ wait: !!flags['wait'],
257
+ profile: flags['profile'],
258
+ });
259
+ break;
260
+ }
261
+ case 'pull-env': {
262
+ await runPullEnv({
263
+ slug: positional[0],
264
+ profile: flags['profile'],
265
+ target: typeof flags['target'] === 'string' ? flags['target'] : 'development',
266
+ });
267
+ break;
268
+ }
269
+ case 'commit': {
270
+ // The legacy GitHub-branch commit path (project_commit). Requires a repo.
271
+ const message = commitMessageFrom(flags);
272
+ if (!message || message === 'hiveku-sync: push') {
273
+ console.error('Error: commit requires -m "<commit message>"\n');
152
274
  console.error(HELP);
153
275
  process.exit(1);
154
276
  }
155
277
  await runProjectPush({
156
- slug,
278
+ slug: positional[0],
157
279
  message,
158
280
  branch: flags['branch'],
159
281
  profile: flags['profile'],
@@ -175,8 +297,14 @@ async function main() {
175
297
  }
176
298
  }
177
299
 
178
- main().catch((err) => {
179
- console.error(`hiveku-sync: ${err.message}`);
180
- if (process.env.HIVEKU_SYNC_DEBUG) console.error(err.stack);
181
- process.exit(1);
182
- });
300
+ // Only run the CLI when invoked directly — importing this module (e.g. from
301
+ // tests to reach parseFlags) must not execute main().
302
+ const isEntrypoint =
303
+ process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
304
+ if (isEntrypoint) {
305
+ main().catch((err) => {
306
+ console.error(`hiveku-sync: ${err.message}`);
307
+ if (process.env.HIVEKU_SYNC_DEBUG) console.error(err.stack);
308
+ process.exit(1);
309
+ });
310
+ }
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@hiveku-apps/sync",
3
- "version": "0.1.0",
4
- "description": "Sync a Hiveku account's memory/rules/skills + coder projects into a local folder. Pairs with the Hiveku MCP at core.hiveku.com/mcp so Claude Code can drive Hiveku tools per-folder.",
3
+ "version": "0.3.0",
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",
7
7
  "author": "Hiveku",
8
8
  "homepage": "https://app.hiveku.com",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "https://github.com/locus2012/hiveku-sync.git"
11
+ "url": "git+https://github.com/locus2012/hiveku-sync.git"
12
12
  },
13
13
  "bugs": {
14
14
  "url": "https://github.com/locus2012/hiveku-sync/issues"
@@ -19,7 +19,8 @@
19
19
  "claude",
20
20
  "claude-code",
21
21
  "cli",
22
- "olympus"
22
+ "olympus",
23
+ "codex"
23
24
  ],
24
25
  "bin": {
25
26
  "hiveku-sync": "bin/hiveku-sync.mjs"
@@ -36,6 +37,7 @@
36
37
  "access": "public"
37
38
  },
38
39
  "scripts": {
39
- "start": "node bin/hiveku-sync.mjs"
40
+ "start": "node bin/hiveku-sync.mjs",
41
+ "test": "node --test 'test/*.test.mjs'"
40
42
  }
41
43
  }
package/src/codex.mjs ADDED
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Codex (OpenAI) support for hiveku-sync — the Codex-native equivalents of the
3
+ * Claude Code artifacts, so a Hiveku account folder works identically from the
4
+ * Codex CLI, the Codex IDE extension, and the ChatGPT desktop app (all three
5
+ * read the same files). Opt in with `--codex` on `init`, or run
6
+ * `hiveku-sync codex` in an existing folder.
7
+ *
8
+ * Writes, merge-safely (only our marked regions are rewritten; user content is
9
+ * preserved):
10
+ * CLAUDE.md → AGENTS.md (lean pointer + load-bearing rules; Codex
11
+ * concatenates AGENTS.md with a ~32KiB cap, so the
12
+ * full guide stays in CLAUDE.md and AGENTS.md says to
13
+ * read it)
14
+ * .mcp.json → .codex/config.toml [mcp_servers.*] (the SAME
15
+ * core.hiveku.com/mcp server + the SAME OLYMPUS_API_KEY;
16
+ * inlined when we can resolve the key, else read from
17
+ * the OLYMPUS_API_KEY env var). git-ignored.
18
+ * skill/command knowledge → .agents/skills/<name>/SKILL.md (repo-scoped; Codex
19
+ * prompts are global-only + deprecated)
20
+ * permissions → NO per-repo equivalent (Codex ignores approval/
21
+ * sandbox keys in repo config) — stated in AGENTS.md.
22
+ *
23
+ * This complements the published Hiveku Codex plugin
24
+ * (github.com/locus2012/hiveku-codex-plugin): the plugin ships the generic
25
+ * `hiveku` MCP server + doctrine skills for single-account users who install it
26
+ * once and set HIVEKU_TOKEN; this writes a SELF-CONTAINED per-account folder
27
+ * (its own MCP config + that account's own knowledge) for multi-account / CI /
28
+ * power-user setups. Use one route or the other per folder, not both.
29
+ */
30
+
31
+ import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
32
+ import { existsSync } from 'node:fs';
33
+ import { homedir } from 'node:os';
34
+ import { join, resolve, dirname } from 'node:path';
35
+
36
+ import { loadDotenv } from './util.mjs';
37
+
38
+ const AGENTS_BEGIN = '<!-- hiveku:begin -->';
39
+ const AGENTS_END = '<!-- hiveku:end -->';
40
+ const TOML_BEGIN = '# hiveku:begin (managed by hiveku-sync — do not edit inside)';
41
+ const TOML_END = '# hiveku:end';
42
+ const SKILL_STAMP = '<!-- generated-by: hiveku-sync (mirrored from account knowledge) -->';
43
+
44
+ /** Replace (or append) the marked region inside existing content. */
45
+ function upsertMarkedRegion(existing, begin, end, region) {
46
+ const b = existing.indexOf(begin);
47
+ const e = existing.indexOf(end);
48
+ if (b !== -1 && e !== -1 && e > b) {
49
+ return existing.slice(0, b) + region + existing.slice(e + end.length);
50
+ }
51
+ const sep = existing.length && !existing.endsWith('\n\n') ? (existing.endsWith('\n') ? '\n' : '\n\n') : '';
52
+ return existing + sep + region + '\n';
53
+ }
54
+
55
+ /** AGENTS.md — lean on purpose (Codex caps the instruction chain at ~32KiB). */
56
+ function agentsMdRegion({ slug }) {
57
+ return `${AGENTS_BEGIN}
58
+ # Hiveku — ${slug}
59
+
60
+ This folder is ONE Hiveku account (${slug}). Its MCP server \`hiveku\` (configured in
61
+ \`.codex/config.toml\`) carries this account's API key — never operate a different account from here.
62
+
63
+ **FIRST ACTION every session: read \`CLAUDE.md\` in this folder.** It is the full operating guide
64
+ (written for Claude Code, applies to you identically): account tools, department data, project
65
+ push/deploy flows, and the work-tracking rules. Ignore only its Claude-specific file paths
66
+ (\`.claude/*\`) — your equivalents are \`.agents/skills/*\` (the account's mirrored skills/commands,
67
+ invoked by name) and this file.
68
+
69
+ Non-negotiables (also in CLAUDE.md, restated because they are load-bearing):
70
+ - **Verify identity before ANY write:** \`get_account_info\` must return THIS account.
71
+ - **You are NOT the only writer.** Others push to these same projects while you work. Check what is
72
+ current BEFORE you start (\`project_version_log\`) and AGAIN before you push (\`project_files_status\`
73
+ — \`changed\` = they edited it, \`only_remote\` = they added files you lack). Before any tree-replace
74
+ (\`delete_missing: true\`), \`dry_run\` first and READ the would-delete list — a file you did not send
75
+ may be someone else's NEW work. Never blind-overwrite.
76
+ - **Keep ALL scratch work in \`.hiveku/tmp/\`** — never \`/tmp\` (shared across accounts), your home
77
+ dir, or the repo root.
78
+ - **Never ingest local agent config into a project push.** \`.codex/config.toml\`, \`.mcp.json\`, and
79
+ \`.env*\` carry this account's key/secrets — exclude them from every tar/bulk-save. Secrets belong in
80
+ \`project_secrets_*\`, never in project code. Never read or print \`.env.local\`.
81
+ - **PM tasks are required** — create one when you start, comment as you go, complete it when done,
82
+ attributed to the authenticated user (\`crm_list_users\`).
83
+ - **Every completed task ends with an "Owner update"** — 2–4 calm, plain-language sentences a busy owner
84
+ can skim: benefit first, no alarm vocabulary, accurate.
85
+ - Video generation is paid + capped — \`marketing_generate_video\` with \`dry_run: true\` first.
86
+
87
+ Discover tools with \`hiveku_docs_search\` / \`hiveku_docs_get\` and \`hiveku_playbooks_list\` /
88
+ \`hiveku_playbook_get\`. Codex ignores repo-level approval/sandbox config, so the rules above are
89
+ instructions, not enforced permissions — follow them.
90
+ ${AGENTS_END}`;
91
+ }
92
+
93
+ /** The [mcp_servers.*] region for <folder>/.codex/config.toml. */
94
+ function codexTomlRegion({ baseUrl, profile, apiKey }) {
95
+ const base = baseUrl.replace(/\/+$/, '');
96
+ const url = profile === 'full' ? `${base}/mcp` : `${base}/mcp?profile=${encodeURIComponent(profile)}`;
97
+ // Prefer an inlined key (self-contained, no env setup needed; the file is
98
+ // git-ignored). Fall back to the OLYMPUS_API_KEY env var when we can't
99
+ // resolve a real key at scaffold time (e.g. `init` before the user pastes it).
100
+ const auth = apiKey
101
+ ? `http_headers = { "Authorization" = "Bearer ${apiKey}" }`
102
+ : `bearer_token_env_var = "OLYMPUS_API_KEY"`;
103
+ return `${TOML_BEGIN}
104
+ [mcp_servers.hiveku]
105
+ url = "${url}"
106
+ ${auth}
107
+
108
+ [mcp_servers.playwright]
109
+ command = "npx"
110
+ args = ["-y", "@playwright/mcp@latest"]
111
+ ${TOML_END}`;
112
+ }
113
+
114
+ /** Add lines to .gitignore when missing (non-destructive). */
115
+ async function appendGitignoreLines(folder, lines) {
116
+ const giPath = join(folder, '.gitignore');
117
+ let gi = '';
118
+ try {
119
+ gi = await readFile(giPath, 'utf8');
120
+ } catch {
121
+ /* new file */
122
+ }
123
+ const have = new Set(gi.split('\n').map((l) => l.trim()));
124
+ const missing = lines.filter((l) => !have.has(l));
125
+ if (missing.length === 0) return;
126
+ const sep = gi.length && !gi.endsWith('\n') ? '\n' : '';
127
+ await writeFile(giPath, gi + sep + missing.join('\n') + '\n', 'utf8');
128
+ }
129
+
130
+ /**
131
+ * Mirror the account's pulled skill + command knowledge into repo-scoped Codex
132
+ * skills at .agents/skills/<name>/SKILL.md. Reads the folders `hiveku-sync pull`
133
+ * writes (skills/, commands/). Idempotent: a skill dir without our stamp is
134
+ * left alone (user-authored). Returns the count written.
135
+ */
136
+ export async function mirrorKnowledgeToSkills(folder) {
137
+ let written = 0;
138
+ for (const srcDir of ['skills', 'commands']) {
139
+ const root = join(folder, srcDir);
140
+ if (!existsSync(root)) continue;
141
+ // pull writes <type>/<department>/<slug>.md — walk one level of subdirs + top-level files.
142
+ const files = [];
143
+ for (const entry of await readdir(root, { withFileTypes: true })) {
144
+ if (entry.isDirectory()) {
145
+ for (const f of await readdir(join(root, entry.name))) {
146
+ if (f.endsWith('.md')) files.push(join(root, entry.name, f));
147
+ }
148
+ } else if (entry.name.endsWith('.md')) {
149
+ files.push(join(root, entry.name));
150
+ }
151
+ }
152
+ for (const filePath of files) {
153
+ const src = await readFile(filePath, 'utf8');
154
+ const base = filePath.split('/').pop().replace(/\.md$/, '');
155
+ const name = `hiveku-${srcDir === 'commands' ? 'cmd' : 'skill'}-${base}`.slice(0, 100);
156
+ let description = `Hiveku ${srcDir === 'commands' ? 'command' : 'skill'}: ${base}`;
157
+ let body = src;
158
+ const fm = src.match(/^---\n([\s\S]*?)\n---\n?/);
159
+ if (fm) {
160
+ const d = fm[1].match(/^description:\s*(.+)$/m);
161
+ if (d) description = d[1].trim().replace(/^["']|["']$/g, '');
162
+ body = src.slice(fm[0].length);
163
+ }
164
+ body = body.replace(/\$ARGUMENTS/g, '(the arguments the user gave after the skill name)');
165
+ const skillDir = join(folder, '.agents', 'skills', name);
166
+ const skillPath = join(skillDir, 'SKILL.md');
167
+ try {
168
+ const existing = await readFile(skillPath, 'utf8');
169
+ if (!existing.includes(SKILL_STAMP)) continue; // user-authored — never clobber
170
+ } catch {
171
+ /* new */
172
+ }
173
+ await mkdir(skillDir, { recursive: true });
174
+ const skill = `---\nname: ${name}\ndescription: ${JSON.stringify(description)}\n---\n${SKILL_STAMP}\n\n${body.trimStart()}`;
175
+ await writeFile(skillPath, skill, 'utf8');
176
+ written += 1;
177
+ }
178
+ }
179
+ return written;
180
+ }
181
+
182
+ /**
183
+ * Write/refresh the Codex scaffold for one account folder: AGENTS.md +
184
+ * .codex/config.toml (git-ignored) + mirror the account's knowledge to skills.
185
+ * Resolves OLYMPUS_API_KEY from the folder's .env / process env to inline the
186
+ * MCP auth when possible.
187
+ */
188
+ export async function writeCodexScaffold({ folder, slug, baseUrl, profile }) {
189
+ const dotenv = await loadDotenv(join(folder, '.env'));
190
+ const apiKey =
191
+ process.env.OLYMPUS_API_KEY ||
192
+ (dotenv.OLYMPUS_API_KEY && dotenv.OLYMPUS_API_KEY !== 'olp_replace_me' ? dotenv.OLYMPUS_API_KEY : '');
193
+
194
+ // AGENTS.md (marker-managed)
195
+ const agentsPath = join(folder, 'AGENTS.md');
196
+ let agents = '';
197
+ try {
198
+ agents = await readFile(agentsPath, 'utf8');
199
+ } catch {
200
+ /* new */
201
+ }
202
+ await writeFile(agentsPath, upsertMarkedRegion(agents, AGENTS_BEGIN, AGENTS_END, agentsMdRegion({ slug })), 'utf8');
203
+
204
+ // .codex/config.toml (marker-managed region; rest of user's config untouched)
205
+ const tomlPath = join(folder, '.codex', 'config.toml');
206
+ let toml = '';
207
+ try {
208
+ toml = await readFile(tomlPath, 'utf8');
209
+ } catch {
210
+ /* new */
211
+ }
212
+ await mkdir(dirname(tomlPath), { recursive: true });
213
+ await writeFile(tomlPath, upsertMarkedRegion(toml, TOML_BEGIN, TOML_END, codexTomlRegion({ baseUrl, profile, apiKey })), 'utf8');
214
+
215
+ await appendGitignoreLines(folder, ['.codex/config.toml']);
216
+
217
+ const skills = await mirrorKnowledgeToSkills(folder);
218
+ return { inlinedKey: !!apiKey, skillsMirrored: skills };
219
+ }
220
+
221
+ /**
222
+ * Pre-trust a folder in the USER-GLOBAL ~/.codex/config.toml so its repo-level
223
+ * .codex/config.toml (which carries the MCP server + key) loads without Codex's
224
+ * first-run trust prompt. Crosses a security boundary — the caller must have
225
+ * explicit user intent (the `--trust` flag).
226
+ */
227
+ export async function preTrustFolder(folder) {
228
+ const cfgPath = join(homedir(), '.codex', 'config.toml');
229
+ let cfg = '';
230
+ try {
231
+ cfg = await readFile(cfgPath, 'utf8');
232
+ } catch {
233
+ /* none yet */
234
+ }
235
+ const abs = resolve(folder);
236
+ const header = `[projects."${abs}"]`;
237
+ if (cfg.includes(header)) return 'already';
238
+ await mkdir(dirname(cfgPath), { recursive: true });
239
+ const sep = cfg.length && !cfg.endsWith('\n') ? '\n' : '';
240
+ await writeFile(cfgPath, `${cfg}${sep}\n${header}\ntrust_level = "trusted"\n`, 'utf8');
241
+ return 'added';
242
+ }