@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.
- package/README.md +115 -30
- package/dist/index.js +332 -89
- 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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
| `--
|
|
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 -->
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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.**
|
|
147
|
-
the LeGreffier skill,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
2732
|
+
urls: [skillUrl(SKILL_VERSION), skillUrl(SKILL_FALLBACK)]
|
|
2731
2733
|
}
|
|
2732
2734
|
];
|
|
2733
|
-
async function downloadSkills(repoDir,
|
|
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
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
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
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
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
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
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
|
-
|
|
2912
|
-
|
|
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 [
|
|
3476
|
-
|
|
3563
|
+
const [cursor, setCursor] = useState(0);
|
|
3564
|
+
const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
|
|
3565
|
+
useInput((input, key) => {
|
|
3477
3566
|
if (key.upArrow) {
|
|
3478
|
-
|
|
3567
|
+
setCursor((i) => i > 0 ? i - 1 : i);
|
|
3479
3568
|
} else if (key.downArrow) {
|
|
3480
|
-
|
|
3481
|
-
} else if (
|
|
3482
|
-
const
|
|
3483
|
-
if (
|
|
3484
|
-
|
|
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((
|
|
3500
|
-
const isCurrent = i ===
|
|
3501
|
-
const
|
|
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: !
|
|
3507
|
-
bold: isCurrent &&
|
|
3508
|
-
children: prefix +
|
|
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: " " +
|
|
3512
|
-
] },
|
|
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: " ↑↓
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 [
|
|
3735
|
-
|
|
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:
|
|
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
|
-
|
|
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: (
|
|
3848
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3883
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
+
"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
|
-
"@
|
|
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",
|