@themoltnet/legreffier 0.3.0 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +115 -30
  2. package/dist/index.js +332 -89
  3. package/package.json +4 -3
package/README.md CHANGED
@@ -35,17 +35,36 @@ npm install -g @themoltnet/legreffier
35
35
  legreffier --name my-agent
36
36
  ```
37
37
 
38
- ### Options
38
+ ### Subcommands
39
+
40
+ #### `legreffier init` (default)
41
+
42
+ Full onboarding: identity, GitHub App, git signing, agent setup.
39
43
 
44
+ ```bash
45
+ legreffier init --name my-agent [--agent claude] [--agent codex]
40
46
  ```
41
- legreffier --name <agent-name> [--api-url <url>] [--dir <path>]
47
+
48
+ #### `legreffier setup`
49
+
50
+ (Re)configure agent tools after init. Reads the existing
51
+ `.moltnet/<name>/moltnet.json` and runs only the agent setup phase.
52
+
53
+ ```bash
54
+ legreffier setup --name my-agent --agent codex
55
+ legreffier setup --name my-agent --agent claude --agent codex
42
56
  ```
43
57
 
44
- | Flag | Description | Default |
45
- | ------------ | ------------------------------------- | ------------------------- |
46
- | `--name, -n` | Agent display name (**required**) | |
47
- | `--api-url` | MoltNet API URL | `https://api.themolt.net` |
48
- | `--dir` | Repository directory for config files | Current working directory |
58
+ ### Options
59
+
60
+ | Flag | Description | Default |
61
+ | ------------- | --------------------------------------- | ------------------------- |
62
+ | `--name, -n` | Agent display name (**required**) | |
63
+ | `--agent, -a` | Agent type(s) to configure (repeatable) | Interactive prompt |
64
+ | `--api-url` | MoltNet API URL | `https://api.themolt.net` |
65
+ | `--dir` | Repository directory for config files | Current working directory |
66
+
67
+ Supported agents: `claude`, `codex`.
49
68
 
50
69
  ## How It Works
51
70
 
@@ -99,10 +118,14 @@ stateDiagram-v2
99
118
 
100
119
  state agent_setup {
101
120
  [*] --> write_config
102
- write_config --> write_mcp_json
103
- write_mcp_json --> download_skills
104
- download_skills --> write_settings_local
105
- write_settings_local --> clear_state
121
+ write_config --> foreach_adapter
122
+ state foreach_adapter {
123
+ [*] --> write_mcp_config
124
+ write_mcp_config --> download_skills
125
+ download_skills --> write_settings
126
+ write_settings --> [*]
127
+ }
128
+ foreach_adapter --> clear_state
106
129
  clear_state --> [*]
107
130
  }
108
131
 
@@ -143,29 +166,51 @@ a standalone gitconfig with `user.name`, `user.email` (GitHub bot noreply),
143
166
  **Phase 4 — Installation.** Opens your browser to install the GitHub App on the
144
167
  repositories you choose. The server confirms and returns OAuth2 credentials.
145
168
 
146
- **Phase 5 — Agent Setup.** Writes all configuration files (see below), downloads
147
- the LeGreffier skill, writes `settings.local.json`, and clears temporary state.
169
+ **Phase 5 — Agent Setup.** For each selected agent type, runs the corresponding
170
+ adapter: writes MCP config, downloads the LeGreffier skill, and writes
171
+ agent-specific settings. Clears temporary state on completion.
148
172
 
149
173
  ## Files Created
150
174
 
175
+ ### Common (all agents)
176
+
151
177
  ```
152
178
  <repo>/
153
179
  ├── .moltnet/<agent-name>/
154
180
  │ ├── moltnet.json # Identity, keys, OAuth2, endpoints, git, GitHub
155
181
  │ ├── gitconfig # Git identity + SSH commit signing
182
+ │ ├── env # Sourceable env vars (used by Codex)
156
183
  │ ├── <app-slug>.pem # GitHub App private key (mode 0600)
157
184
  │ └── ssh/
158
185
  │ ├── id_ed25519 # SSH private key (mode 0600)
159
186
  │ └── id_ed25519.pub # SSH public key
187
+ ```
188
+
189
+ ### Claude Code (`--agent claude`)
190
+
191
+ ```
192
+ <repo>/
160
193
  ├── .mcp.json # MCP server config (env var placeholders)
161
194
  └── .claude/
162
195
  ├── settings.local.json # Credential values (⚠️ gitignore this!)
163
196
  └── skills/legreffier/ # Downloaded LeGreffier skill
164
197
  ```
165
198
 
199
+ ### Codex (`--agent codex`)
200
+
201
+ ```
202
+ <repo>/
203
+ ├── .codex/
204
+ │ └── config.toml # MCP server config with env_http_headers
205
+ └── .agents/
206
+ └── skills/legreffier/ # Downloaded LeGreffier skill
207
+ ```
208
+
166
209
  ### How credentials flow
167
210
 
168
- The CLI writes two files that work together:
211
+ The env var prefix is derived from the agent name: `my-agent` → `MY_AGENT`.
212
+
213
+ **Claude Code** uses two files that work together:
169
214
 
170
215
  1. **`.claude/settings.local.json`** — contains credential values in clear text:
171
216
 
@@ -203,20 +248,58 @@ The CLI writes two files that work together:
203
248
  > **Important:** `settings.local.json` contains secrets in clear text. Make sure
204
249
  > `.claude/settings.local.json` is in your `.gitignore`.
205
250
 
206
- The env var prefix is derived from the agent name: `my-agent` `MY_AGENT`.
251
+ **Codex** uses `.codex/config.toml` with `env_http_headers` that reference env
252
+ var names. The actual values must be in the shell environment — the CLI writes
253
+ them to `.moltnet/<name>/env` for easy sourcing:
254
+
255
+ ```toml
256
+ [mcp_servers.my-agent]
257
+ url = "https://mcp.themolt.net/mcp"
258
+
259
+ [mcp_servers.my-agent.env_http_headers]
260
+ X-Client-Id = "MY_AGENT_CLIENT_ID"
261
+ X-Client-Secret = "MY_AGENT_CLIENT_SECRET"
262
+ ```
207
263
 
208
- ## Launching Claude Code
264
+ > **Important:** `.moltnet/<name>/env` contains secrets in clear text. Make sure
265
+ > it is in your `.gitignore`.
266
+
267
+ ## Launching Your Agent
268
+
269
+ ### Claude Code
209
270
 
210
271
  ```bash
211
272
  claude
212
273
  ```
213
274
 
214
- That's it. Claude Code loads `settings.local.json` automatically, resolves the
215
- `${VAR}` placeholders in `.mcp.json`, and connects to the MCP server.
275
+ Claude Code loads `settings.local.json` automatically, resolves the `${VAR}`
276
+ placeholders in `.mcp.json`, and connects to the MCP server.
277
+
278
+ ### Codex
279
+
280
+ Codex needs the credentials as shell env vars. Source the env file before
281
+ launching:
282
+
283
+ ```bash
284
+ set -a && . .moltnet/<agent-name>/env && set +a
285
+ GIT_CONFIG_GLOBAL=.moltnet/<agent-name>/gitconfig codex
286
+ ```
287
+
288
+ Or use a package.json script (as in this repo):
289
+
290
+ ```json
291
+ {
292
+ "scripts": {
293
+ "codex": "set -a && . .moltnet/my-agent/env && set +a && GIT_CONFIG_GLOBAL=.moltnet/my-agent/gitconfig codex"
294
+ }
295
+ }
296
+ ```
297
+
298
+ Then just `pnpm codex`.
216
299
 
217
300
  ## Activation
218
301
 
219
- Once inside a Claude Code session:
302
+ Once inside a Claude Code or Codex session:
220
303
 
221
304
  ```
222
305
  /legreffier
@@ -239,12 +322,6 @@ git push origin <branch>
239
322
  On GitHub, commits show the app's logo as avatar, the agent display name, and
240
323
  SSH signature verification.
241
324
 
242
- ## Multi-Agent Support
243
-
244
- Currently `legreffier init` writes Claude Code configuration. Support for
245
- additional AI coding agents (Cursor, Codex, Cline) is planned — see
246
- [#324](https://github.com/getlarge/themoltnet/issues/324).
247
-
248
325
  ## Advanced: Manual Setup
249
326
 
250
327
  For finer control over each step:
@@ -317,24 +394,32 @@ permission.
317
394
 
318
395
  ### MCP tools unavailable
319
396
 
320
- Check that `settings.local.json` exists and has the correct values. Then verify
321
- Claude Code loaded them:
397
+ **Claude Code:** Check that `settings.local.json` exists and has the correct
398
+ values. Then verify Claude Code loaded them:
322
399
 
323
400
  ```bash
324
401
  # Inside Claude Code
325
402
  echo $MY_AGENT_CLIENT_ID
326
403
  ```
327
404
 
405
+ **Codex:** Verify the env file exists and is sourced before launch:
406
+
407
+ ```bash
408
+ cat .moltnet/<agent-name>/env # Check credentials exist
409
+ echo $MY_AGENT_CLIENT_ID # Check env is loaded
410
+ cat .codex/config.toml # Check MCP config
411
+ ```
412
+
328
413
  ### Resume after interruption
329
414
 
330
- Re-run the same `legreffier --name <agent-name>` command. Completed phases are
331
- skipped automatically.
415
+ Re-run the same `legreffier init --name <agent-name>` command. Completed phases
416
+ are skipped automatically.
332
417
 
333
418
  ### Start fresh
334
419
 
335
420
  ```bash
336
421
  rm -rf .moltnet/<agent-name>/
337
- legreffier --name <agent-name>
422
+ legreffier init --name <agent-name>
338
423
  ```
339
424
 
340
425
  ## License
package/dist/index.js CHANGED
@@ -5,9 +5,10 @@ import { useInput, Box, Text, useApp, render } from "ink";
5
5
  import { join } from "node:path";
6
6
  import { useState, useEffect, useReducer, useRef } from "react";
7
7
  import figlet from "figlet";
8
- import { readFile, mkdir, writeFile, chmod, rm } from "node:fs/promises";
8
+ import { readFile, writeFile, mkdir, chmod, rm } from "node:fs/promises";
9
9
  import { homedir } from "node:os";
10
10
  import { createHash, randomBytes as randomBytes$1 } from "crypto";
11
+ import { parse, stringify } from "smol-toml";
11
12
  import open from "open";
12
13
  const colors = {
13
14
  // Primary — teal/cyan (The Network)
@@ -2719,31 +2720,43 @@ async function pollUntil(baseUrl, workflowId, targetStatuses, onTick) {
2719
2720
  `Timed out waiting for status: ${targetStatuses.join(" or ")}`
2720
2721
  );
2721
2722
  }
2722
- const SKILL_DIRS = {
2723
- claude: ".claude/skills"
2724
- // codex: '.agents/skills',
2725
- };
2726
2723
  const SKILL_VERSION = "legreffier-v0.1.0";
2724
+ const SKILL_FALLBACK = "main";
2725
+ const SKILL_PATH = ".claude/skills/legreffier/SKILL.md";
2726
+ function skillUrl(ref) {
2727
+ return `https://raw.githubusercontent.com/getlarge/themoltnet/${ref}/${SKILL_PATH}`;
2728
+ }
2727
2729
  const SKILLS = [
2728
2730
  {
2729
2731
  name: "legreffier",
2730
- url: `https://raw.githubusercontent.com/getlarge/themoltnet/${SKILL_VERSION}/.claude/skills/legreffier/SKILL.md`
2732
+ urls: [skillUrl(SKILL_VERSION), skillUrl(SKILL_FALLBACK)]
2731
2733
  }
2732
2734
  ];
2733
- async function downloadSkills(repoDir, agentTypes) {
2734
- const dirs = agentTypes.map((t) => SKILL_DIRS[t]).filter((d) => !!d);
2735
- if (dirs.length === 0) return;
2735
+ async function downloadSkills(repoDir, skillDir) {
2736
2736
  for (const skill of SKILLS) {
2737
- const res = await fetch(skill.url);
2738
- if (!res.ok) {
2739
- throw new Error(`Failed to download skill ${skill.name} (${res.status})`);
2737
+ let content = null;
2738
+ for (const url of skill.urls) {
2739
+ let res;
2740
+ try {
2741
+ res = await fetch(url);
2742
+ } catch {
2743
+ continue;
2744
+ }
2745
+ if (res.ok) {
2746
+ content = await res.text();
2747
+ break;
2748
+ }
2740
2749
  }
2741
- const content = await res.text();
2742
- for (const skillDir of dirs) {
2743
- const destDir = join(repoDir, skillDir, skill.name);
2744
- await mkdir(destDir, { recursive: true });
2745
- await writeFile(join(destDir, "SKILL.md"), content, "utf-8");
2750
+ if (!content) {
2751
+ process.stderr.write(
2752
+ `Warning: could not download skill "${skill.name}", skipping.
2753
+ `
2754
+ );
2755
+ continue;
2746
2756
  }
2757
+ const destDir = join(repoDir, skillDir, skill.name);
2758
+ await mkdir(destDir, { recursive: true });
2759
+ await writeFile(join(destDir, "SKILL.md"), content, "utf-8");
2747
2760
  }
2748
2761
  }
2749
2762
  function buildPermissions(agentName) {
@@ -2811,6 +2824,88 @@ async function writeSettingsLocal({
2811
2824
  };
2812
2825
  await writeFile(filePath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
2813
2826
  }
2827
+ class ClaudeAdapter {
2828
+ type = "claude";
2829
+ async writeMcpConfig(opts) {
2830
+ await writeMcpConfig(
2831
+ {
2832
+ mcpServers: {
2833
+ [opts.agentName]: {
2834
+ type: "http",
2835
+ url: opts.mcpUrl,
2836
+ headers: {
2837
+ "X-Client-Id": `\${${opts.prefix}_CLIENT_ID}`,
2838
+ "X-Client-Secret": `\${${opts.prefix}_CLIENT_SECRET}`
2839
+ }
2840
+ }
2841
+ }
2842
+ },
2843
+ opts.repoDir
2844
+ );
2845
+ }
2846
+ async writeSkills(repoDir) {
2847
+ await downloadSkills(repoDir, ".claude/skills");
2848
+ }
2849
+ async writeSettings(opts) {
2850
+ await writeSettingsLocal({
2851
+ repoDir: opts.repoDir,
2852
+ agentName: opts.agentName,
2853
+ appSlug: opts.appSlug,
2854
+ pemPath: opts.pemPath,
2855
+ installationId: opts.installationId,
2856
+ clientId: opts.clientId,
2857
+ clientSecret: opts.clientSecret
2858
+ });
2859
+ }
2860
+ }
2861
+ class CodexAdapter {
2862
+ type = "codex";
2863
+ async writeMcpConfig(opts) {
2864
+ const dir2 = join(opts.repoDir, ".codex");
2865
+ await mkdir(dir2, { recursive: true });
2866
+ const filePath = join(dir2, "config.toml");
2867
+ let existing = {};
2868
+ try {
2869
+ const raw = await readFile(filePath, "utf-8");
2870
+ existing = parse(raw);
2871
+ } catch {
2872
+ }
2873
+ const servers = existing.mcp_servers ?? {};
2874
+ servers[opts.agentName] = {
2875
+ url: opts.mcpUrl,
2876
+ env_http_headers: {
2877
+ "X-Client-Id": `${opts.prefix}_CLIENT_ID`,
2878
+ "X-Client-Secret": `${opts.prefix}_CLIENT_SECRET`
2879
+ }
2880
+ };
2881
+ const merged = { ...existing, mcp_servers: servers };
2882
+ await writeFile(filePath, stringify(merged) + "\n", "utf-8");
2883
+ }
2884
+ async writeSkills(repoDir) {
2885
+ await downloadSkills(repoDir, ".agents/skills");
2886
+ }
2887
+ /**
2888
+ * Write a sourceable env file at `.moltnet/<name>/env` with the OAuth2
2889
+ * credentials that Codex needs in the shell environment.
2890
+ */
2891
+ async writeSettings(opts) {
2892
+ const envDir = join(opts.repoDir, ".moltnet", opts.agentName);
2893
+ await mkdir(envDir, { recursive: true });
2894
+ const q = (v) => `'${v.replace(/'/g, "'\\''")}'`;
2895
+ const lines = [
2896
+ `${opts.prefix}_CLIENT_ID=${q(opts.clientId)}`,
2897
+ `${opts.prefix}_CLIENT_SECRET=${q(opts.clientSecret)}`,
2898
+ `${opts.prefix}_GITHUB_APP_ID=${q(opts.appSlug)}`,
2899
+ `${opts.prefix}_GITHUB_APP_PRIVATE_KEY_PATH=${q(opts.pemPath)}`,
2900
+ `${opts.prefix}_GITHUB_APP_INSTALLATION_ID=${q(opts.installationId)}`
2901
+ ];
2902
+ await writeFile(join(envDir, "env"), lines.join("\n") + "\n", "utf-8");
2903
+ }
2904
+ }
2905
+ const adapters = {
2906
+ claude: new ClaudeAdapter(),
2907
+ codex: new CodexAdapter()
2908
+ };
2814
2909
  function getStatePath(configDir) {
2815
2910
  return join(configDir, "legreffier-init.state.json");
2816
2911
  }
@@ -2879,38 +2974,31 @@ async function runAgentSetupPhase(opts) {
2879
2974
  configDir
2880
2975
  );
2881
2976
  }
2882
- if (clientId) {
2883
- const prefix = toEnvPrefix(agentName);
2884
- const mcpUrl = apiUrl2.replace("://api.", "://mcp.") + "/mcp";
2885
- await writeMcpConfig(
2886
- {
2887
- mcpServers: {
2888
- [agentName]: {
2889
- type: "http",
2890
- url: mcpUrl,
2891
- headers: {
2892
- "X-Client-Id": `\${${prefix}_CLIENT_ID}`,
2893
- "X-Client-Secret": `\${${prefix}_CLIENT_SECRET}`
2894
- }
2895
- }
2896
- }
2897
- },
2898
- repoDir
2899
- );
2900
- }
2901
- dispatch({ type: "step", key: "skills", status: "running" });
2902
- await downloadSkills(repoDir, agentTypes);
2903
- dispatch({ type: "step", key: "skills", status: "done" });
2904
- dispatch({ type: "step", key: "settings", status: "running" });
2905
- await writeSettingsLocal({
2977
+ const prefix = toEnvPrefix(agentName);
2978
+ const mcpUrl = apiUrl2.replace("://api.", "://mcp.") + "/mcp";
2979
+ const adapterOpts = {
2906
2980
  repoDir,
2907
2981
  agentName,
2982
+ prefix,
2983
+ mcpUrl,
2984
+ clientId,
2985
+ clientSecret,
2908
2986
  appSlug,
2909
2987
  pemPath,
2910
- installationId,
2911
- clientId,
2912
- clientSecret
2913
- });
2988
+ installationId
2989
+ };
2990
+ dispatch({ type: "step", key: "skills", status: "running" });
2991
+ for (const agentType of agentTypes) {
2992
+ const adapter = adapters[agentType];
2993
+ await adapter.writeMcpConfig(adapterOpts);
2994
+ await adapter.writeSkills(repoDir);
2995
+ }
2996
+ dispatch({ type: "step", key: "skills", status: "done" });
2997
+ dispatch({ type: "step", key: "settings", status: "running" });
2998
+ for (const agentType of agentTypes) {
2999
+ const adapter = adapters[agentType];
3000
+ await adapter.writeSettings(adapterOpts);
3001
+ }
2914
3002
  dispatch({ type: "step", key: "settings", status: "done" });
2915
3003
  await clearState(configDir);
2916
3004
  }
@@ -3450,7 +3538,7 @@ async function runInstallationPhase(opts) {
3450
3538
  clientSecret: result.clientSecret ?? ""
3451
3539
  };
3452
3540
  }
3453
- const SUPPORTED_AGENTS = ["claude"];
3541
+ const SUPPORTED_AGENTS = ["claude", "codex"];
3454
3542
  const AGENTS = [
3455
3543
  {
3456
3544
  id: "claude",
@@ -3458,31 +3546,43 @@ const AGENTS = [
3458
3546
  description: "settings.local.json + .mcp.json + /legreffier skill",
3459
3547
  available: true
3460
3548
  },
3461
- {
3462
- id: "cursor",
3463
- label: "Cursor",
3464
- description: "coming soon",
3465
- available: false
3466
- },
3467
3549
  {
3468
3550
  id: "codex",
3469
3551
  label: "Codex",
3552
+ description: ".codex/config.toml + .agents/skills/ + /legreffier skill",
3553
+ available: true
3554
+ },
3555
+ {
3556
+ id: "cursor",
3557
+ label: "Cursor",
3470
3558
  description: "coming soon",
3471
3559
  available: false
3472
3560
  }
3473
3561
  ];
3474
3562
  function AgentSelect({ onSelect }) {
3475
- const [selected, setSelected] = useState(0);
3476
- useInput((_input, key) => {
3563
+ const [cursor, setCursor] = useState(0);
3564
+ const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
3565
+ useInput((input, key) => {
3477
3566
  if (key.upArrow) {
3478
- setSelected((i) => i > 0 ? i - 1 : i);
3567
+ setCursor((i) => i > 0 ? i - 1 : i);
3479
3568
  } else if (key.downArrow) {
3480
- setSelected((i) => i < AGENTS.length - 1 ? i + 1 : i);
3481
- } else if (key.return) {
3482
- const agent2 = AGENTS[selected];
3483
- if (agent2?.available && SUPPORTED_AGENTS.includes(agent2.id)) {
3484
- onSelect(agent2.id);
3569
+ setCursor((i) => i < AGENTS.length - 1 ? i + 1 : i);
3570
+ } else if (input === " ") {
3571
+ const agent = AGENTS[cursor];
3572
+ if (agent?.available && SUPPORTED_AGENTS.includes(agent.id)) {
3573
+ setSelected((prev) => {
3574
+ const next = new Set(prev);
3575
+ const id = agent.id;
3576
+ if (next.has(id)) {
3577
+ next.delete(id);
3578
+ } else {
3579
+ next.add(id);
3580
+ }
3581
+ return next;
3582
+ });
3485
3583
  }
3584
+ } else if (key.return && selected.size > 0) {
3585
+ onSelect(Array.from(selected));
3486
3586
  }
3487
3587
  });
3488
3588
  return /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsxs(
@@ -3494,25 +3594,27 @@ function AgentSelect({ onSelect }) {
3494
3594
  paddingX: 2,
3495
3595
  paddingY: 1,
3496
3596
  children: [
3497
- /* @__PURE__ */ jsx(Text, { color: cliTheme.color.primary, bold: true, children: "Select your AI coding agent" }),
3597
+ /* @__PURE__ */ jsx(Text, { color: cliTheme.color.primary, bold: true, children: "Select your AI coding agent(s)" }),
3498
3598
  /* @__PURE__ */ jsx(Text, { children: " " }),
3499
- AGENTS.map((agent2, i) => {
3500
- const isCurrent = i === selected;
3501
- const prefix = isCurrent ? "▸ " : " ";
3599
+ AGENTS.map((agent, i) => {
3600
+ const isCurrent = i === cursor;
3601
+ const isSelected = selected.has(agent.id);
3602
+ const checkbox = agent.available ? isSelected ? "[*] " : "[ ] " : " ";
3603
+ const prefix = isCurrent ? "> " : " ";
3502
3604
  return /* @__PURE__ */ jsxs(Box, { children: [
3503
3605
  /* @__PURE__ */ jsx(
3504
3606
  Text,
3505
3607
  {
3506
- color: !agent2.available ? cliTheme.color.muted : isCurrent ? cliTheme.color.accent : cliTheme.color.text,
3507
- bold: isCurrent && agent2.available,
3508
- children: prefix + agent2.label
3608
+ color: !agent.available ? cliTheme.color.muted : isCurrent ? cliTheme.color.accent : cliTheme.color.text,
3609
+ bold: isCurrent && agent.available,
3610
+ children: prefix + checkbox + agent.label
3509
3611
  }
3510
3612
  ),
3511
- /* @__PURE__ */ jsx(Text, { color: cliTheme.color.muted, children: " " + agent2.description })
3512
- ] }, agent2.id);
3613
+ /* @__PURE__ */ jsx(Text, { color: cliTheme.color.muted, children: " " + agent.description })
3614
+ ] }, agent.id);
3513
3615
  }),
3514
3616
  /* @__PURE__ */ jsx(Text, { children: " " }),
3515
- /* @__PURE__ */ jsx(Text, { color: cliTheme.color.muted, children: " ↑↓ to navigate, Enter to select" })
3617
+ /* @__PURE__ */ jsx(Text, { color: cliTheme.color.muted, children: " ↑↓ navigate, Space toggle, Enter confirm" })
3516
3618
  ]
3517
3619
  }
3518
3620
  ) });
@@ -3564,7 +3666,7 @@ function isFuturePhase(current, target) {
3564
3666
  return WORK_PHASES.indexOf(target) > WORK_PHASES.indexOf(current);
3565
3667
  }
3566
3668
  function DisclaimerPhase({
3567
- selectedAgent,
3669
+ hasAgents,
3568
3670
  onAccept,
3569
3671
  onSelectAgent,
3570
3672
  onReject
@@ -3574,7 +3676,7 @@ function DisclaimerPhase({
3574
3676
  /* @__PURE__ */ jsx(
3575
3677
  CliDisclaimer,
3576
3678
  {
3577
- onAccept: selectedAgent ? onAccept : onSelectAgent,
3679
+ onAccept: hasAgents ? onAccept : onSelectAgent,
3578
3680
  onReject
3579
3681
  }
3580
3682
  )
@@ -3720,7 +3822,7 @@ function ProgressPhase({
3720
3822
  }
3721
3823
  function InitApp({
3722
3824
  name: name2,
3723
- agent: agentProp,
3825
+ agents: agentsProp,
3724
3826
  apiUrl: apiUrl2,
3725
3827
  dir: dir2 = process.cwd()
3726
3828
  }) {
@@ -3731,8 +3833,8 @@ function InitApp({
3731
3833
  steps: initialSteps
3732
3834
  });
3733
3835
  const [accepted, setAccepted] = useState(false);
3734
- const [selectedAgent, setSelectedAgent] = useState(
3735
- agentProp ?? null
3836
+ const [selectedAgents, setSelectedAgents] = useState(
3837
+ agentsProp ?? []
3736
3838
  );
3737
3839
  const [showManifestFallback, setShowManifestFallback] = useState(false);
3738
3840
  const [showInstallFallback, setShowInstallFallback] = useState(false);
@@ -3801,7 +3903,7 @@ function InitApp({
3801
3903
  repoDir: dir2,
3802
3904
  configDir,
3803
3905
  agentName: name2,
3804
- agentTypes: selectedAgent ? [selectedAgent] : [],
3906
+ agentTypes: selectedAgents,
3805
3907
  publicKey: identity.publicKey,
3806
3908
  fingerprint: identity.fingerprint,
3807
3909
  appSlug: githubApp.appSlug,
@@ -3835,7 +3937,7 @@ function InitApp({
3835
3937
  disclaimer: () => /* @__PURE__ */ jsx(
3836
3938
  DisclaimerPhase,
3837
3939
  {
3838
- selectedAgent,
3940
+ hasAgents: selectedAgents.length > 0,
3839
3941
  onAccept: () => setAccepted(true),
3840
3942
  onSelectAgent: () => dispatch({ type: "phase", phase: "agent_select" }),
3841
3943
  onReject: () => exit()
@@ -3844,8 +3946,8 @@ function InitApp({
3844
3946
  agent_select: () => /* @__PURE__ */ jsx(
3845
3947
  AgentSelectPhase,
3846
3948
  {
3847
- onSelect: (agent2) => {
3848
- setSelectedAgent(agent2);
3949
+ onSelect: (selected) => {
3950
+ setSelectedAgents(selected);
3849
3951
  setAccepted(true);
3850
3952
  }
3851
3953
  }
@@ -3865,23 +3967,142 @@ function InitApp({
3865
3967
  }
3866
3968
  );
3867
3969
  }
3868
- const { values } = parseArgs({
3970
+ function SetupApp({
3971
+ name: name2,
3972
+ agents: agentsProp,
3973
+ apiUrl: apiUrl2,
3974
+ dir: dir2
3975
+ }) {
3976
+ const { exit } = useApp();
3977
+ const [phase, setPhase] = useState(
3978
+ agentsProp.length > 0 ? "running" : "agent_select"
3979
+ );
3980
+ const [agents2, setAgents] = useState(agentsProp);
3981
+ const [error, setError] = useState();
3982
+ const [filesWritten, setFilesWritten] = useState([]);
3983
+ const [summary, setSummary] = useState(null);
3984
+ useEffect(() => {
3985
+ if (phase !== "running" || agents2.length === 0) return;
3986
+ void (async () => {
3987
+ try {
3988
+ const configDir = join(dir2, ".moltnet", name2);
3989
+ const config = await readConfig(configDir);
3990
+ if (!config) {
3991
+ throw new Error(
3992
+ `Config not found at ${configDir}/moltnet.json. Run "legreffier init" first.`
3993
+ );
3994
+ }
3995
+ const prefix = toEnvPrefix(name2);
3996
+ const mcpUrl = config.endpoints?.mcp ?? apiUrl2.replace("://api.", "://mcp.") + "/mcp";
3997
+ const opts = {
3998
+ repoDir: dir2,
3999
+ agentName: name2,
4000
+ prefix,
4001
+ mcpUrl,
4002
+ clientId: config.oauth2.client_id,
4003
+ clientSecret: config.oauth2.client_secret,
4004
+ appSlug: config.github?.app_slug ?? config.github?.app_id ?? "",
4005
+ pemPath: config.github?.private_key_path ?? "",
4006
+ installationId: config.github?.installation_id ?? ""
4007
+ };
4008
+ const written = [];
4009
+ for (const agentType of agents2) {
4010
+ const adapter = adapters[agentType];
4011
+ await adapter.writeMcpConfig(opts);
4012
+ written.push(`${agentType}: MCP config`);
4013
+ await adapter.writeSkills(dir2);
4014
+ written.push(`${agentType}: skills`);
4015
+ await adapter.writeSettings(opts);
4016
+ written.push(`${agentType}: settings`);
4017
+ }
4018
+ setFilesWritten(written);
4019
+ setSummary({
4020
+ agentName: name2,
4021
+ fingerprint: config.keys?.fingerprint ?? "",
4022
+ appSlug: config.github?.app_slug ?? config.github?.app_id ?? "",
4023
+ apiUrl: config.endpoints?.api ?? apiUrl2,
4024
+ mcpUrl
4025
+ });
4026
+ setPhase("done");
4027
+ setTimeout(() => exit(), 3e3);
4028
+ } catch (err2) {
4029
+ setError(err2 instanceof Error ? err2.message : String(err2));
4030
+ setPhase("error");
4031
+ setTimeout(() => exit(new Error("Setup failed")), 3e3);
4032
+ }
4033
+ })();
4034
+ }, [phase, agents2]);
4035
+ if (phase === "agent_select") {
4036
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingY: 1, children: [
4037
+ /* @__PURE__ */ jsx(CliHero, {}),
4038
+ /* @__PURE__ */ jsx(
4039
+ AgentSelect,
4040
+ {
4041
+ onSelect: (selected) => {
4042
+ setAgents(selected);
4043
+ setPhase("running");
4044
+ }
4045
+ }
4046
+ )
4047
+ ] });
4048
+ }
4049
+ if (phase === "running") {
4050
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingY: 1, children: [
4051
+ /* @__PURE__ */ jsx(CliHero, {}),
4052
+ /* @__PURE__ */ jsx(CliSpinner, { label: `Configuring ${agents2.join(", ")} for ${name2}...` })
4053
+ ] });
4054
+ }
4055
+ if (phase === "error") {
4056
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingY: 1, children: [
4057
+ /* @__PURE__ */ jsx(CliHero, {}),
4058
+ /* @__PURE__ */ jsx(
4059
+ Box,
4060
+ {
4061
+ borderStyle: "round",
4062
+ borderColor: cliTheme.color.error,
4063
+ paddingX: 2,
4064
+ paddingY: 1,
4065
+ children: /* @__PURE__ */ jsx(Text, { color: cliTheme.color.error, bold: true, children: "* Setup failed: " + (error ?? "unknown error") })
4066
+ }
4067
+ )
4068
+ ] });
4069
+ }
4070
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingY: 1, children: [
4071
+ /* @__PURE__ */ jsx(CliHero, {}),
4072
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
4073
+ /* @__PURE__ */ jsx(Text, { color: cliTheme.color.success, bold: true, children: "Agent setup complete!" }),
4074
+ filesWritten.map((f, i) => /* @__PURE__ */ jsx(Text, { color: cliTheme.color.muted, children: " * " + f }, i))
4075
+ ] }),
4076
+ summary && /* @__PURE__ */ jsx(
4077
+ CliSummaryBox,
4078
+ {
4079
+ agentName: summary.agentName,
4080
+ fingerprint: summary.fingerprint,
4081
+ appSlug: summary.appSlug,
4082
+ apiUrl: summary.apiUrl,
4083
+ mcpUrl: summary.mcpUrl
4084
+ }
4085
+ )
4086
+ ] });
4087
+ }
4088
+ const { values, positionals } = parseArgs({
3869
4089
  args: process.argv.slice(2),
4090
+ allowPositionals: true,
3870
4091
  options: {
3871
4092
  name: { type: "string", short: "n" },
3872
- agent: { type: "string", short: "a" },
4093
+ agent: { type: "string", short: "a", multiple: true },
3873
4094
  "api-url": { type: "string" },
3874
4095
  dir: { type: "string" }
3875
4096
  }
3876
4097
  });
4098
+ const subcommand = positionals[0] ?? "init";
3877
4099
  const name = values["name"];
3878
- const agentFlag = values["agent"];
4100
+ const agentFlags = values["agent"] ?? [];
3879
4101
  const apiUrl = values["api-url"] ?? process.env.MOLTNET_API_URL ?? "https://api.themolt.net";
3880
4102
  const dir = values["dir"] ?? process.cwd();
3881
4103
  if (!name) {
3882
- process.stderr.write(
3883
- "Usage: legreffier --name <agent-name> [--agent claude] [--api-url <url>] [--dir <path>]\n"
3884
- );
4104
+ const usage = subcommand === "setup" ? "Usage: legreffier setup --name <agent-name> [--agent claude] [--agent codex] [--dir <path>]" : "Usage: legreffier [init] --name <agent-name> [--agent claude] [--agent codex] [--api-url <url>] [--dir <path>]";
4105
+ process.stderr.write(usage + "\n");
3885
4106
  process.exit(1);
3886
4107
  }
3887
4108
  const AGENT_NAME_RE = /^[a-z0-9][a-z0-9-]{0,37}[a-z0-9]$/;
@@ -3892,12 +4113,34 @@ if (!AGENT_NAME_RE.test(name)) {
3892
4113
  );
3893
4114
  process.exit(1);
3894
4115
  }
3895
- if (agentFlag && !SUPPORTED_AGENTS.includes(agentFlag)) {
4116
+ for (const a of agentFlags) {
4117
+ if (!SUPPORTED_AGENTS.includes(a)) {
4118
+ process.stderr.write(
4119
+ `Unsupported agent: ${a}. Supported: ${SUPPORTED_AGENTS.join(", ")}
4120
+ `
4121
+ );
4122
+ process.exit(1);
4123
+ }
4124
+ }
4125
+ const agents = agentFlags;
4126
+ if (subcommand === "setup") {
4127
+ render(/* @__PURE__ */ jsx(SetupApp, { name, agents, apiUrl, dir }));
4128
+ } else if (subcommand === "init") {
4129
+ render(
4130
+ /* @__PURE__ */ jsx(
4131
+ InitApp,
4132
+ {
4133
+ name,
4134
+ agents: agents.length > 0 ? agents : void 0,
4135
+ apiUrl,
4136
+ dir
4137
+ }
4138
+ )
4139
+ );
4140
+ } else {
3896
4141
  process.stderr.write(
3897
- `Unsupported agent: ${agentFlag}. Supported: ${SUPPORTED_AGENTS.join(", ")}
4142
+ `Unknown subcommand: ${subcommand}. Use "init" or "setup".
3898
4143
  `
3899
4144
  );
3900
4145
  process.exit(1);
3901
4146
  }
3902
- const agent = agentFlag;
3903
- render(/* @__PURE__ */ jsx(InitApp, { name, agent, apiUrl, dir }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/legreffier",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "LeGreffier — one-command accountable AI agent setup",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -24,10 +24,11 @@
24
24
  "ink": "^6.8.0",
25
25
  "open": "^10.1.2",
26
26
  "react": "^19.0.0",
27
+ "smol-toml": "^1.6.0",
27
28
  "@moltnet/api-client": "0.1.0",
28
- "@moltnet/crypto-service": "0.1.0",
29
29
  "@moltnet/design-system": "0.1.0",
30
- "@themoltnet/sdk": "0.46.0"
30
+ "@moltnet/crypto-service": "0.1.0",
31
+ "@themoltnet/sdk": "0.47.0"
31
32
  },
32
33
  "devDependencies": {
33
34
  "@types/figlet": "^1.7.0",