@themoltnet/legreffier 0.31.0 → 0.32.1

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 +6 -3
  2. package/dist/index.js +502 -397
  3. package/package.json +6 -7
package/README.md CHANGED
@@ -36,16 +36,19 @@ and across agents.
36
36
 
37
37
  ```bash
38
38
  # Run directly (no install needed)
39
- npx @themoltnet/legreffier --name my-agent
39
+ npx @themoltnet/legreffier init --name my-agent
40
40
 
41
41
  # Or install globally
42
42
  npm install -g @themoltnet/legreffier
43
- legreffier --name my-agent
43
+ legreffier init --name my-agent
44
44
  ```
45
45
 
46
+ Every invocation requires an explicit subcommand (`init`, `setup`, `port`,
47
+ or `github`). Run `legreffier --help` to see them all.
48
+
46
49
  ### Subcommands
47
50
 
48
- #### `legreffier init` (default)
51
+ #### `legreffier init`
49
52
 
50
53
  Full onboarding: identity, GitHub App, git signing, agent setup.
51
54
 
package/dist/index.js CHANGED
@@ -1,263 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import { statSync } from "node:fs";
3
- import { parseArgs, parseEnv } from "node:util";
4
- import { Box, Text, render, useApp, useInput } from "ink";
2
+ import { defineCommand, runMain } from "citty";
5
3
  import { execFileSync, execSync } from "node:child_process";
6
4
  import { basename, dirname, isAbsolute, join } from "node:path";
5
+ import { Box, Text, render, useApp, useInput } from "ink";
7
6
  import { useEffect, useReducer, useRef, useState } from "react";
8
7
  import { jsx, jsxs } from "react/jsx-runtime";
9
- import figlet from "figlet";
10
8
  import { createSign } from "node:crypto";
11
9
  import { createHash, randomBytes } from "crypto";
12
10
  import { access, chmod, copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
13
11
  import { homedir } from "node:os";
14
12
  import { parse, stringify } from "smol-toml";
13
+ import { parseEnv } from "node:util";
15
14
  import open from "open";
16
- //#region src/commands/help.ts
17
- function formatFlag(flag) {
18
- const head = flag.short ? `${flag.name}, ${flag.short}` : flag.name;
19
- const value = flag.value ? ` ${flag.value}` : "";
20
- const suffixParts = [];
21
- if (flag.required) suffixParts.push("(required)");
22
- if (flag.default !== void 0) suffixParts.push(`default: ${flag.default}`);
23
- return ` ${head}${value}${suffixParts.length > 0 ? ` [${suffixParts.join(", ")}]` : ""}\n ${flag.description}`;
24
- }
25
- function printCommandHelp(help) {
26
- const lines = [];
27
- lines.push(`${help.command} — ${help.summary}`);
28
- lines.push("");
29
- lines.push(`Usage: ${help.usage}`);
30
- lines.push("");
31
- lines.push(help.description);
32
- if (help.flags.length > 0) {
33
- lines.push("");
34
- lines.push("Flags:");
35
- for (const flag of help.flags) lines.push(formatFlag(flag));
36
- }
37
- if (help.examples.length > 0) {
38
- lines.push("");
39
- lines.push("Examples:");
40
- for (const ex of help.examples) {
41
- lines.push(` # ${ex.description}`);
42
- lines.push(` ${ex.command}`);
43
- }
44
- }
45
- if (help.notes && help.notes.length > 0) {
46
- lines.push("");
47
- lines.push("Notes:");
48
- for (const note of help.notes) lines.push(` - ${note}`);
49
- }
50
- lines.push("");
51
- process.stdout.write(lines.join("\n"));
52
- }
53
- function printRootHelp(commands) {
54
- const lines = [];
55
- lines.push("legreffier — LeGreffier CLI");
56
- lines.push("");
57
- lines.push("Usage: legreffier <command> [flags]");
58
- lines.push("");
59
- lines.push("Commands:");
60
- const pad = Math.max(...commands.map((c) => c.command.length)) + 2;
61
- for (const cmd of commands) lines.push(` ${cmd.command.padEnd(pad)}${cmd.summary}`);
62
- lines.push("");
63
- lines.push("Run `legreffier <command> --help` for command-specific help.");
64
- lines.push("");
65
- process.stdout.write(lines.join("\n"));
66
- }
67
- var COMMANDS = [
68
- {
69
- command: "init",
70
- summary: "Create a new agent identity and wire it into this repository",
71
- usage: "legreffier init --name <agent-name> [flags]",
72
- description: "Runs the full 5-phase onboarding: generates an Ed25519 keypair, registers the agent on MoltNet, creates a GitHub App via manifest flow, writes the gitconfig with SSH signing, installs the GitHub App on selected repos, and writes the MCP config for your chosen agent clients.",
73
- flags: [
74
- {
75
- name: "--name",
76
- short: "-n",
77
- value: "<agent-name>",
78
- description: "Agent name (2-39 lowercase alphanumerics/hyphens, e.g. `jobi`)",
79
- required: true
80
- },
81
- {
82
- name: "--agent",
83
- short: "-a",
84
- value: "claude|codex",
85
- description: "Agent client to configure (repeatable). Default: no client config written."
86
- },
87
- {
88
- name: "--api-url",
89
- value: "<url>",
90
- description: "MoltNet API base URL",
91
- default: "https://api.themolt.net"
92
- },
93
- {
94
- name: "--dir",
95
- value: "<path>",
96
- description: "Target repository root",
97
- default: "current working directory"
98
- },
99
- {
100
- name: "--org",
101
- short: "-o",
102
- value: "<github-org>",
103
- description: "GitHub organization to install the App on (optional)"
104
- }
105
- ],
106
- examples: [{
107
- description: "Basic init for a new agent named `jobi`",
108
- command: "legreffier init --name jobi --agent claude"
109
- }, {
110
- description: "Init against a local API",
111
- command: "legreffier init --name jobi --agent claude --api-url http://localhost:3000"
112
- }]
113
- },
114
- {
115
- command: "setup",
116
- summary: "Install LeGreffier skills and MCP config into an existing repo",
117
- usage: "legreffier setup --name <agent-name> [flags]",
118
- description: "For a repository that already has `.moltnet/<agent-name>/` credentials (e.g. after running `init` elsewhere), `setup` writes the MCP config, downloads skills, and configures your agent clients. Does not touch identity, keys, or GitHub App state.",
119
- flags: [
120
- {
121
- name: "--name",
122
- short: "-n",
123
- value: "<agent-name>",
124
- description: "Agent name (must already exist under `.moltnet/`)",
125
- required: true
126
- },
127
- {
128
- name: "--agent",
129
- short: "-a",
130
- value: "claude|codex",
131
- description: "Agent client to configure (repeatable)"
132
- },
133
- {
134
- name: "--dir",
135
- value: "<path>",
136
- description: "Target repository root",
137
- default: "current working directory"
138
- }
139
- ],
140
- examples: [{
141
- description: "Install skills and MCP config for both Claude and Codex",
142
- command: "legreffier setup --name jobi --agent claude --agent codex"
143
- }]
144
- },
145
- {
146
- command: "port",
147
- summary: "Copy an existing agent from another repository into this one",
148
- usage: "legreffier port --name <agent-name> --from <repo-root>/.moltnet/<agent-name> [flags]",
149
- description: "Ports an existing agent identity (keypair, moltnet.json, gitconfig, GitHub App credentials) from a source repository into the current one. `--from` is strict: it must point to the exact `<repo-root>/.moltnet/<agent-name>` directory. The source repo is not modified.",
150
- flags: [
151
- {
152
- name: "--name",
153
- short: "-n",
154
- value: "<agent-name>",
155
- description: "Agent name to port (must exist under `--from`)",
156
- required: true
157
- },
158
- {
159
- name: "--from",
160
- value: "<repo-root>/.moltnet/<agent-name>",
161
- description: "Absolute path to the source agent directory. Strict format: must be `<repo-root>/.moltnet/<agent-name>` and contain moltnet.json + gitconfig.",
162
- required: true
163
- },
164
- {
165
- name: "--agent",
166
- short: "-a",
167
- value: "claude|codex",
168
- description: "Agent client to configure in the target repo (repeatable)",
169
- default: "claude"
170
- },
171
- {
172
- name: "--dir",
173
- value: "<path>",
174
- description: "Target repository root",
175
- default: "current working directory"
176
- },
177
- {
178
- name: "--diary",
179
- value: "new|reuse|skip",
180
- description: "How to handle the diary in the new repo: `new` creates a fresh diary, `reuse` reuses the source diary ID, `skip` leaves MOLTNET_DIARY_ID unset",
181
- default: "new"
182
- }
183
- ],
184
- examples: [{
185
- description: "Port agent `jobi` from a sibling repo",
186
- command: "legreffier port --name jobi --from /Users/me/code/other-repo/.moltnet/jobi"
187
- }, {
188
- description: "Port and reuse the existing diary",
189
- command: "legreffier port --name jobi --from /Users/me/code/other-repo/.moltnet/jobi --diary reuse"
190
- }],
191
- notes: ["The source repo is read-only; nothing there is modified.", "`--from` does not accept relative paths, `~`, or repo-name shorthands. Provide the full `.moltnet/<agent>` directory path."]
192
- }
193
- ];
194
- //#endregion
195
- //#region src/commands/resolveCommand.ts
196
- /**
197
- * Long-form option names that consume the next argument as a value.
198
- * Used by `resolveHelpCommand` to skip over option values when scanning
199
- * for the first positional subcommand.
200
- */
201
- var VALUE_OPTIONS_LONG = new Set([
202
- "--name",
203
- "--agent",
204
- "--api-url",
205
- "--dir",
206
- "--org",
207
- "--from",
208
- "--diary"
209
- ]);
210
- /**
211
- * Short-form option names that consume the next argument as a value.
212
- * Kept in sync with `parseArgs` options in index.tsx.
213
- */
214
- var VALUE_OPTIONS_SHORT = new Set([
215
- "-n",
216
- "-a",
217
- "-o"
218
- ]);
219
- function isValueOption(arg) {
220
- if (VALUE_OPTIONS_LONG.has(arg)) return true;
221
- if (VALUE_OPTIONS_SHORT.has(arg)) return true;
222
- return false;
223
- }
224
- /**
225
- * Resolve which command's help to print for `legreffier <...> --help`.
226
- *
227
- * Scans `rawArgs` linearly, skipping option flags and their values, and
228
- * returns the first positional argument that matches a known command. If
229
- * no positional is found (or the positional is not a known command),
230
- * returns `null` so the caller prints root help.
231
- *
232
- * Unlike `rawArgs.find((a) => !a.startsWith('-'))`, this correctly handles
233
- * flags-before-subcommand orderings like:
234
- *
235
- * legreffier --name jobi port --help
236
- *
237
- * where the naive scan would return "jobi" instead of "port".
238
- *
239
- * Unknown positionals (typos, etc.) fall back to root help rather than
240
- * silently matching nothing, so users see the full command list.
241
- */
242
- function resolveHelpCommand(rawArgs, commands) {
243
- for (let i = 0; i < rawArgs.length; i++) {
244
- const arg = rawArgs[i];
245
- if (arg === void 0) continue;
246
- if (arg === "--help" || arg === "-h") continue;
247
- if (arg.startsWith("--") && arg.includes("=")) continue;
248
- if (arg.startsWith("--")) {
249
- if (isValueOption(arg)) i++;
250
- continue;
251
- }
252
- if (arg.startsWith("-") && arg.length > 1) {
253
- if (isValueOption(arg)) i++;
254
- continue;
255
- }
256
- return commands.find((c) => c.command === arg) ?? null;
257
- }
258
- return null;
259
- }
260
- //#endregion
15
+ import { statSync } from "node:fs";
261
16
  //#region src/github-token.ts
262
17
  var MOLTNET_GITCONFIG_RE = /\.moltnet\/([^/]+)\/gitconfig$/;
263
18
  function resolveAgentName(nameFlag, gitConfigGlobal) {
@@ -289,6 +44,146 @@ function printGitHubToken(agentName, dir) {
289
44
  process.stdout.write(token);
290
45
  }
291
46
  //#endregion
47
+ //#region src/ui/types.ts
48
+ var SUPPORTED_AGENTS = ["claude", "codex"];
49
+ //#endregion
50
+ //#region src/commands/shared.ts
51
+ /**
52
+ * Regex for valid agent names: 2-39 chars, lowercase alphanumerics and
53
+ * hyphens, must start and end with an alphanumeric. Matches the server-side
54
+ * validation so a client-side failure gives immediate feedback rather than
55
+ * a REST 400 after a keypair has been generated.
56
+ */
57
+ var AGENT_NAME_RE = /^[a-z0-9][a-z0-9-]{0,37}[a-z0-9]$/;
58
+ var DEFAULT_API_URL = "https://api.themolt.net";
59
+ /**
60
+ * Common argument definitions shared across subcommands. Kept as plain
61
+ * objects (not a merged record) so individual commands can pick the flags
62
+ * they actually accept instead of inheriting every flag.
63
+ */
64
+ var commonArgs = {
65
+ name: {
66
+ type: "string",
67
+ description: "Agent name (2-39 lowercase alphanumerics or hyphens, e.g. `jobi`)",
68
+ alias: "n",
69
+ valueHint: "agent-name"
70
+ },
71
+ agent: {
72
+ type: "string",
73
+ description: "Agent client to configure (repeatable: --agent claude --agent codex). Accepts: claude, codex.",
74
+ alias: "a",
75
+ valueHint: "claude|codex"
76
+ },
77
+ "api-url": {
78
+ type: "string",
79
+ description: "MoltNet API base URL (default: $MOLTNET_API_URL or https://api.themolt.net)",
80
+ valueHint: "url"
81
+ },
82
+ dir: {
83
+ type: "string",
84
+ description: "Target repository root (default: current working directory)",
85
+ valueHint: "path"
86
+ }
87
+ };
88
+ /**
89
+ * Collect repeated `--agent` / `-a` values from rawArgs.
90
+ *
91
+ * Citty wraps Node's `parseArgs` without `multiple: true`, which means
92
+ * repeating `--agent claude --agent codex` keeps only the last value. The
93
+ * hand-rolled CLI supported repeats and users' docs rely on that shape, so
94
+ * we walk `rawArgs` ourselves to rebuild the full list before validating it
95
+ * against the supported-agent allowlist.
96
+ */
97
+ function collectAgents(rawArgs) {
98
+ const out = [];
99
+ for (let i = 0; i < rawArgs.length; i++) {
100
+ const token = rawArgs[i];
101
+ if (token === "--") break;
102
+ let value;
103
+ if (token === "--agent" || token === "-a") {
104
+ value = rawArgs[i + 1];
105
+ i++;
106
+ } else if (token.startsWith("--agent=")) value = token.slice(8);
107
+ else if (token.startsWith("-a=")) value = token.slice(3);
108
+ if (value === void 0) continue;
109
+ if (!SUPPORTED_AGENTS.includes(value)) throw new CliValidationError(`Unsupported agent: ${value}. Supported: ${SUPPORTED_AGENTS.join(", ")}`);
110
+ out.push(value);
111
+ }
112
+ return out;
113
+ }
114
+ /**
115
+ * Validate a `--name` arg. Throws with the same user-facing message as the
116
+ * previous hand-rolled CLI so scripts and docs referencing it keep working.
117
+ */
118
+ function requireAgentName(name) {
119
+ if (typeof name !== "string" || name.length === 0) throw new CliValidationError("--name is required");
120
+ if (!AGENT_NAME_RE.test(name)) throw new CliValidationError(`Invalid agent name: "${name}". Must be 2-39 lowercase alphanumeric characters or hyphens, starting and ending with a letter or digit.`);
121
+ return name;
122
+ }
123
+ /** Resolve the target repo dir, defaulting to CWD. */
124
+ function resolveDir(dir) {
125
+ if (typeof dir === "string" && dir.length > 0) return dir;
126
+ return process.cwd();
127
+ }
128
+ /** Resolve the API URL: --api-url flag > $MOLTNET_API_URL > default. */
129
+ function resolveApiUrl(apiUrl) {
130
+ if (typeof apiUrl === "string" && apiUrl.length > 0) return apiUrl;
131
+ return process.env["MOLTNET_API_URL"] ?? DEFAULT_API_URL;
132
+ }
133
+ /**
134
+ * Thrown by shared validators (`requireAgentName`, `collectAgents`,
135
+ * `validatePortFromArg` adapters, etc.) when the user passed a bad flag
136
+ * value. `withCleanErrors` catches these and prints a single-line
137
+ * "Error: <msg>" on stderr + exit 1, instead of letting citty dump the
138
+ * full stack via its default `console.error(error, "\n")` handler.
139
+ */
140
+ var CliValidationError = class extends Error {
141
+ name = "CliValidationError";
142
+ };
143
+ /**
144
+ * Wrap a command handler so `CliValidationError`s print a single clean
145
+ * line and exit 1. Unexpected errors (bugs, TypeErrors, network failures)
146
+ * still bubble up with their full stack so we can debug them.
147
+ */
148
+ function withCleanErrors(handler) {
149
+ return async (ctx) => {
150
+ try {
151
+ await handler(ctx);
152
+ } catch (err) {
153
+ if (err instanceof CliValidationError) {
154
+ process.stderr.write(`Error: ${err.message}\n`);
155
+ process.exit(1);
156
+ }
157
+ throw err;
158
+ }
159
+ };
160
+ }
161
+ var githubCommand = defineCommand({
162
+ meta: {
163
+ name: "github",
164
+ description: "GitHub-related helpers (token minting)"
165
+ },
166
+ subCommands: { token: defineCommand({
167
+ meta: {
168
+ name: "token",
169
+ description: "Print a short-lived installation token for the agent GitHub App (reads .moltnet/<agent>/moltnet.json)."
170
+ },
171
+ args: {
172
+ name: commonArgs.name,
173
+ dir: commonArgs.dir
174
+ },
175
+ run: withCleanErrors(({ args }) => {
176
+ let agentName;
177
+ try {
178
+ agentName = resolveAgentName(typeof args.name === "string" ? args.name : void 0, process.env["GIT_CONFIG_GLOBAL"]);
179
+ } catch (err) {
180
+ throw new CliValidationError(err instanceof Error ? err.message : String(err));
181
+ }
182
+ printGitHubToken(agentName, resolveDir(args.dir));
183
+ })
184
+ }) }
185
+ });
186
+ //#endregion
292
187
  //#region ../../libs/design-system/src/tokens.ts
293
188
  /**
294
189
  * MoltNet Design Tokens
@@ -572,7 +467,7 @@ var QUILL_LINES = [
572
467
  " ╲───────╲───────╲",
573
468
  " ◆"
574
469
  ];
575
- var WORDMARK$1 = [
470
+ var WORDMARK = [
576
471
  " __ __ ___ _ _____ _ _ ___ _____",
577
472
  "| \\/ |/ _ \\| ||_ _|| \\| | __|_ _|",
578
473
  "| |\\/| | (_) | |__| | | .` | _| | | ",
@@ -619,7 +514,7 @@ function CliHero({ animated = false }) {
619
514
  color: glowColor,
620
515
  children: HALO_TOP
621
516
  }),
622
- WORDMARK$1.map((line, i) => /* @__PURE__ */ jsxs(Text, { children: [
517
+ WORDMARK.map((line, i) => /* @__PURE__ */ jsxs(Text, { children: [
623
518
  /* @__PURE__ */ jsx(Text, {
624
519
  color: glowColor,
625
520
  children: " · · │ "
@@ -680,7 +575,6 @@ function CliHero({ animated = false }) {
680
575
  })
681
576
  });
682
577
  }
683
- figlet.textSync("MOLTNET", { font: "slant" });
684
578
  //#endregion
685
579
  //#region ../../libs/design-system/src/cli/CliSpinner.tsx
686
580
  var FRAMES = [
@@ -8189,6 +8083,85 @@ async function writeEnvFile(opts) {
8189
8083
  }
8190
8084
  await writeFile(envPath, outputLines.join("\n") + "\n", "utf-8");
8191
8085
  }
8086
+ /**
8087
+ * Update a single managed key in an existing env file.
8088
+ * If the key exists, its value is replaced; otherwise appended.
8089
+ */
8090
+ async function updateEnvVar(envDir, key, value) {
8091
+ const envPath = join(envDir, "env");
8092
+ let content;
8093
+ try {
8094
+ content = await readFile(envPath, "utf-8");
8095
+ } catch {
8096
+ content = "";
8097
+ }
8098
+ const line = `${key}=${q(value)}`;
8099
+ const keyPrefix = `${key}=`;
8100
+ const lines = content === "" ? [] : content.split("\n");
8101
+ const index = lines.findIndex((l) => l.startsWith(keyPrefix));
8102
+ if (index !== -1) {
8103
+ lines[index] = line;
8104
+ content = lines.join("\n");
8105
+ } else content = content.endsWith("\n") ? content + line + "\n" : content + "\n" + line + "\n";
8106
+ await writeFile(envPath, content, "utf-8");
8107
+ }
8108
+ /**
8109
+ * Resolve the human operator's git identity from global git config.
8110
+ * Must be called BEFORE GIT_CONFIG_GLOBAL is set (so it reads the
8111
+ * human's config, not the agent's).
8112
+ *
8113
+ * Returns `"Name <email>"` or `null` if either is missing.
8114
+ */
8115
+ function resolveHumanGitIdentity() {
8116
+ try {
8117
+ const name = execFileSync("git", [
8118
+ "config",
8119
+ "--global",
8120
+ "user.name"
8121
+ ], {
8122
+ encoding: "utf-8",
8123
+ env: {
8124
+ ...process.env,
8125
+ GIT_CONFIG_GLOBAL: void 0
8126
+ }
8127
+ }).trim();
8128
+ const email = execFileSync("git", [
8129
+ "config",
8130
+ "--global",
8131
+ "user.email"
8132
+ ], {
8133
+ encoding: "utf-8",
8134
+ env: {
8135
+ ...process.env,
8136
+ GIT_CONFIG_GLOBAL: void 0
8137
+ }
8138
+ }).trim();
8139
+ return name && email ? `${name} <${email}>` : null;
8140
+ } catch {
8141
+ return null;
8142
+ }
8143
+ }
8144
+ /**
8145
+ * Append MOLTNET_HUMAN_GIT_IDENTITY and optionally MOLTNET_COMMIT_AUTHORSHIP
8146
+ * to an existing env file if not already present.
8147
+ * Preserves existing content — only appends new vars.
8148
+ */
8149
+ async function appendAuthorshipVars(envDir, humanGitIdentity, commitAuthorship) {
8150
+ const envPath = join(envDir, "env");
8151
+ let existing = "";
8152
+ try {
8153
+ existing = await readFile(envPath, "utf-8");
8154
+ } catch {
8155
+ return;
8156
+ }
8157
+ const lines = [];
8158
+ const hasVar = (content, key) => new RegExp(`^${key}=`, "m").test(content);
8159
+ if (humanGitIdentity && !hasVar(existing, "MOLTNET_HUMAN_GIT_IDENTITY")) lines.push(`MOLTNET_HUMAN_GIT_IDENTITY=${q(humanGitIdentity)}`);
8160
+ if (commitAuthorship && !hasVar(existing, "MOLTNET_COMMIT_AUTHORSHIP")) lines.push(`MOLTNET_COMMIT_AUTHORSHIP=${q(commitAuthorship)}`);
8161
+ if (lines.length === 0) return;
8162
+ const suffix = lines.join("\n") + "\n";
8163
+ await writeFile(envPath, existing.endsWith("\n") ? existing + suffix : existing + "\n" + suffix, "utf-8");
8164
+ }
8192
8165
  //#endregion
8193
8166
  //#region src/state.ts
8194
8167
  function getStatePath(configDir) {
@@ -8293,6 +8266,7 @@ async function runAgentSetupPhase(opts) {
8293
8266
  pemPath,
8294
8267
  installationId
8295
8268
  });
8269
+ await appendAuthorshipVars(configDir, opts.humanGitIdentity ?? resolveHumanGitIdentity(), opts.commitAuthorship);
8296
8270
  await clearState(configDir);
8297
8271
  }
8298
8272
  //#endregion
@@ -8577,10 +8551,41 @@ async function writeTokenCache(cachePath, token, expiresAt) {
8577
8551
  } catch {}
8578
8552
  }
8579
8553
  /**
8580
- * Exchange a GitHub App JWT for an installation access token.
8581
- * Uses a file-based cache next to the private key to avoid
8582
- * hitting the GitHub API on every call.
8554
+ * List all installations of this GitHub App and return the one whose
8555
+ * `account.login` matches the given owner (case-insensitive).
8556
+ *
8557
+ * Uses the App JWT (not an installation token), so it works even when
8558
+ * `installation_id` is missing or stale.
8583
8559
  */
8560
+ async function findInstallationForOwner(opts) {
8561
+ const privateKeyPem = await readFile(opts.privateKeyPath, "utf-8");
8562
+ const jwt = createAppJWT(opts.appId, privateKeyPem);
8563
+ const ownerLower = opts.owner.toLowerCase();
8564
+ let nextUrl = "https://api.github.com/app/installations?per_page=100";
8565
+ let pageCount = 0;
8566
+ const MAX_PAGES = 10;
8567
+ while (nextUrl && pageCount < MAX_PAGES) {
8568
+ pageCount++;
8569
+ const res = await fetch(nextUrl, { headers: {
8570
+ Authorization: `Bearer ${jwt}`,
8571
+ Accept: "application/vnd.github+json",
8572
+ "X-GitHub-Api-Version": "2022-11-28"
8573
+ } });
8574
+ if (!res.ok) throw new Error(`GitHub API error listing installations (${res.status}): ${await res.text()}`);
8575
+ const match = (await res.json()).find((i) => i.account?.login.toLowerCase() === ownerLower);
8576
+ if (match) return { installationId: String(match.id) };
8577
+ const linkHeader = res.headers.get("link");
8578
+ nextUrl = linkHeader ? parseNextLinkHeader(linkHeader) : null;
8579
+ }
8580
+ return null;
8581
+ }
8582
+ function parseNextLinkHeader(header) {
8583
+ for (const part of header.split(",")) {
8584
+ const match = part.match(/<([^>]+)>\s*;\s*rel="next"/);
8585
+ if (match) return match[1];
8586
+ }
8587
+ return null;
8588
+ }
8584
8589
  async function getInstallationToken(opts) {
8585
8590
  const cachePath = join(dirname(opts.privateKeyPath), "gh-token-cache.json");
8586
8591
  const cached = await readTokenCache(cachePath);
@@ -8851,9 +8856,6 @@ async function runInstallationPhase(opts) {
8851
8856
  };
8852
8857
  }
8853
8858
  //#endregion
8854
- //#region src/ui/types.ts
8855
- var SUPPORTED_AGENTS = ["claude", "codex"];
8856
- //#endregion
8857
8859
  //#region src/ui/AgentSelect.tsx
8858
8860
  var AGENTS = [
8859
8861
  {
@@ -9093,12 +9095,26 @@ function ProgressPhase({ state, name, showManifestFallback, showInstallFallback
9093
9095
  flexDirection: "column",
9094
9096
  children: steps.githubApp === "running" ? /* @__PURE__ */ jsxs(Box, {
9095
9097
  flexDirection: "column",
9096
- children: [/* @__PURE__ */ jsx(CliSpinner, { label: githubAppSpinnerLabel }), showManifestFallback && manifestFormUrl ? /* @__PURE__ */ jsxs(Text, {
9097
- color: cliTheme.color.muted,
9098
- children: [" → ", /* @__PURE__ */ jsx(Text, {
9099
- color: cliTheme.color.accent,
9100
- children: manifestFormUrl
9101
- })]
9098
+ children: [/* @__PURE__ */ jsx(CliSpinner, { label: githubAppSpinnerLabel }), manifestFormUrl ? /* @__PURE__ */ jsxs(Box, {
9099
+ flexDirection: "column",
9100
+ children: [
9101
+ /* @__PURE__ */ jsxs(Text, {
9102
+ color: cliTheme.color.text,
9103
+ children: [" ", "Confirm the GitHub App creation in your browser:"]
9104
+ }),
9105
+ /* @__PURE__ */ jsxs(Text, {
9106
+ color: cliTheme.color.accent,
9107
+ children: [
9108
+ " ",
9109
+ "→ ",
9110
+ manifestFormUrl
9111
+ ]
9112
+ }),
9113
+ showManifestFallback ? /* @__PURE__ */ jsxs(Text, {
9114
+ color: cliTheme.color.muted,
9115
+ children: [" ", "Browser didn't open? Copy the link above."]
9116
+ }) : null
9117
+ ]
9102
9118
  }) : null]
9103
9119
  }) : /* @__PURE__ */ jsx(CliStatusLine, {
9104
9120
  label: "Create GitHub App",
@@ -9125,12 +9141,26 @@ function ProgressPhase({ state, name, showManifestFallback, showInstallFallback
9125
9141
  children: [
9126
9142
  steps.installation === "running" ? /* @__PURE__ */ jsxs(Box, {
9127
9143
  flexDirection: "column",
9128
- children: [/* @__PURE__ */ jsx(CliSpinner, { label: installationSpinnerLabel }), showInstallFallback && installationUrl ? /* @__PURE__ */ jsxs(Text, {
9129
- color: cliTheme.color.muted,
9130
- children: [" → ", /* @__PURE__ */ jsx(Text, {
9131
- color: cliTheme.color.accent,
9132
- children: installationUrl
9133
- })]
9144
+ children: [/* @__PURE__ */ jsx(CliSpinner, { label: installationSpinnerLabel }), installationUrl ? /* @__PURE__ */ jsxs(Box, {
9145
+ flexDirection: "column",
9146
+ children: [
9147
+ /* @__PURE__ */ jsxs(Text, {
9148
+ color: cliTheme.color.text,
9149
+ children: [" ", "Install the GitHub App on your account/org:"]
9150
+ }),
9151
+ /* @__PURE__ */ jsxs(Text, {
9152
+ color: cliTheme.color.accent,
9153
+ children: [
9154
+ " ",
9155
+ "→ ",
9156
+ installationUrl
9157
+ ]
9158
+ }),
9159
+ showInstallFallback ? /* @__PURE__ */ jsxs(Text, {
9160
+ color: cliTheme.color.muted,
9161
+ children: [" ", "Browser didn't open? Copy the link above."]
9162
+ }) : null
9163
+ ]
9134
9164
  }) : null]
9135
9165
  }) : /* @__PURE__ */ jsx(CliStatusLine, {
9136
9166
  label: "GitHub App installation",
@@ -9288,6 +9318,43 @@ function InitApp({ name, agents: agentsProp, apiUrl, dir = process.cwd(), org })
9288
9318
  });
9289
9319
  }
9290
9320
  //#endregion
9321
+ //#region src/commands/init.tsx
9322
+ var initCommand = defineCommand({
9323
+ meta: {
9324
+ name: "init",
9325
+ description: "Create a new agent identity and wire it into this repository"
9326
+ },
9327
+ args: {
9328
+ name: {
9329
+ ...commonArgs.name,
9330
+ required: true
9331
+ },
9332
+ agent: commonArgs.agent,
9333
+ "api-url": commonArgs["api-url"],
9334
+ dir: commonArgs.dir,
9335
+ org: {
9336
+ type: "string",
9337
+ description: "GitHub organization to install the App on (optional)",
9338
+ alias: "o",
9339
+ valueHint: "github-org"
9340
+ }
9341
+ },
9342
+ run: withCleanErrors(({ args, rawArgs }) => {
9343
+ const name = requireAgentName(args.name);
9344
+ const agents = collectAgents(rawArgs);
9345
+ const apiUrl = resolveApiUrl(args["api-url"]);
9346
+ const dir = resolveDir(args.dir);
9347
+ const org = typeof args.org === "string" ? args.org : void 0;
9348
+ render(/* @__PURE__ */ jsx(InitApp, {
9349
+ name,
9350
+ agents: agents.length > 0 ? agents : void 0,
9351
+ apiUrl,
9352
+ dir,
9353
+ org
9354
+ }));
9355
+ })
9356
+ });
9357
+ //#endregion
9291
9358
  //#region src/phases/portArgs.ts
9292
9359
  /**
9293
9360
  * Validate the raw `--from` argument passed to `legreffier port` before
@@ -9540,6 +9607,62 @@ async function runPortDiaryPhase(opts) {
9540
9607
  };
9541
9608
  }
9542
9609
  //#endregion
9610
+ //#region src/phases/portResolveInstallation.ts
9611
+ /**
9612
+ * Resolve the correct `installation_id` for the target owner.
9613
+ *
9614
+ * When porting a config across orgs the source `installation_id` is scoped
9615
+ * to the original account. This phase uses the App JWT to list all
9616
+ * installations and find the one matching the target owner, then updates
9617
+ * `moltnet.json` if it differs.
9618
+ */
9619
+ async function runPortResolveInstallationPhase(opts) {
9620
+ const { targetDir, config, currentRepo } = opts;
9621
+ if (!currentRepo) return {
9622
+ status: "skipped",
9623
+ message: "unable to determine target repo — skipping installation_id resolution",
9624
+ installationId: config.github?.installation_id ?? ""
9625
+ };
9626
+ if (!config.github?.app_id || !config.github?.private_key_path) return {
9627
+ status: "skipped",
9628
+ message: "github.app_id or private_key_path missing — cannot resolve",
9629
+ installationId: config.github?.installation_id ?? ""
9630
+ };
9631
+ const targetOwner = currentRepo.split("/")[0];
9632
+ let result;
9633
+ try {
9634
+ result = await findInstallationForOwner({
9635
+ appId: config.github.app_id,
9636
+ privateKeyPath: config.github.private_key_path,
9637
+ owner: targetOwner
9638
+ });
9639
+ } catch (err) {
9640
+ return {
9641
+ status: "skipped",
9642
+ message: `could not list app installations: ${err.message}`,
9643
+ installationId: config.github?.installation_id ?? ""
9644
+ };
9645
+ }
9646
+ if (!result) return {
9647
+ status: "not-installed",
9648
+ message: `GitHub App is not installed on ${targetOwner} — install it first`,
9649
+ installationId: config.github?.installation_id ?? ""
9650
+ };
9651
+ const oldId = config.github.installation_id;
9652
+ if (oldId === result.installationId) return {
9653
+ status: "unchanged",
9654
+ message: `installation_id ${oldId} already matches ${targetOwner}`,
9655
+ installationId: oldId
9656
+ };
9657
+ await updateConfigSection("github", { installation_id: result.installationId }, targetDir);
9658
+ if (opts.envPrefix) await updateEnvVar(targetDir, `${opts.envPrefix}_GITHUB_APP_INSTALLATION_ID`, result.installationId);
9659
+ return {
9660
+ status: "updated",
9661
+ message: `installation_id updated: ${oldId || "(empty)"} → ${result.installationId} (${targetOwner})`,
9662
+ installationId: result.installationId
9663
+ };
9664
+ }
9665
+ //#endregion
9543
9666
  //#region src/phases/portRewrite.ts
9544
9667
  /**
9545
9668
  * Rewrite absolute paths in the ported `moltnet.json` so they point to
@@ -9602,6 +9725,7 @@ async function runPortRewritePhase(opts) {
9602
9725
  pemPath: newPem,
9603
9726
  installationId: config.github.installation_id
9604
9727
  });
9728
+ await appendAuthorshipVars(targetDir, resolveHumanGitIdentity());
9605
9729
  return {
9606
9730
  configPath: join(targetDir, "moltnet.json"),
9607
9731
  rewrittenFields,
@@ -9762,6 +9886,16 @@ function PortApp({ name, agents, sourceDir, targetRepoDir, diaryMode, apiUrl })
9762
9886
  });
9763
9887
  filesWritten.push(rewriteResult.gitConfigPath);
9764
9888
  filesWritten.push(join(targetDir, "env"));
9889
+ setPhase("resolving_installation");
9890
+ const currentRepo = detectCurrentRepo(targetRepoDir);
9891
+ const prefix = toEnvPrefix(name);
9892
+ const resolveResult = await runPortResolveInstallationPhase({
9893
+ targetDir,
9894
+ config: await readConfig(targetDir) ?? config,
9895
+ currentRepo: currentRepo ?? void 0,
9896
+ envPrefix: prefix
9897
+ });
9898
+ if (resolveResult.status === "not-installed" || resolveResult.status === "skipped") warnings.push(resolveResult.message);
9765
9899
  setPhase("diary");
9766
9900
  const diaryResult = await runPortDiaryPhase({
9767
9901
  targetDir,
@@ -9772,14 +9906,14 @@ function PortApp({ name, agents, sourceDir, targetRepoDir, diaryMode, apiUrl })
9772
9906
  const adapterOpts = {
9773
9907
  repoDir: targetRepoDir,
9774
9908
  agentName: name,
9775
- prefix: toEnvPrefix(name),
9909
+ prefix,
9776
9910
  mcpUrl: config.endpoints?.mcp ?? apiUrl.replace("://api.", "://mcp.") + "/mcp",
9777
9911
  clientId: config.oauth2.client_id,
9778
9912
  clientSecret: config.oauth2.client_secret,
9779
9913
  appSlug: config.github?.app_slug ?? "",
9780
9914
  appId: config.github?.app_id ?? "",
9781
9915
  pemPath: join(targetDir, basename(config.github?.private_key_path ?? "")),
9782
- installationId: config.github?.installation_id ?? ""
9916
+ installationId: resolveResult.installationId
9783
9917
  };
9784
9918
  for (const agentType of agents) {
9785
9919
  const adapter = adapters[agentType];
@@ -9795,7 +9929,7 @@ function PortApp({ name, agents, sourceDir, targetRepoDir, diaryMode, apiUrl })
9795
9929
  setPhase("verifying");
9796
9930
  const verifyResult = await runPortVerifyInstallationPhase({
9797
9931
  config: await readConfig(targetDir) ?? config,
9798
- currentRepo: detectCurrentRepo(targetRepoDir) ?? void 0
9932
+ currentRepo: currentRepo ?? void 0
9799
9933
  });
9800
9934
  if (verifyResult.status !== "ok") warnings.push(verifyResult.message);
9801
9935
  setSummary({
@@ -9837,6 +9971,7 @@ function PortApp({ name, agents, sourceDir, targetRepoDir, diaryMode, apiUrl })
9837
9971
  validating: `Validating source .moltnet/${name}...`,
9838
9972
  copying: `Copying private material...`,
9839
9973
  rewriting: `Rewriting paths in moltnet.json...`,
9974
+ resolving_installation: `Resolving GitHub App installation for target org...`,
9840
9975
  diary: `Configuring diary (${diaryMode})...`,
9841
9976
  agent_setup: `Installing agent files for ${agents.join(", ")}...`,
9842
9977
  verifying: `Verifying GitHub App installation scope...`
@@ -9894,6 +10029,68 @@ function PortApp({ name, agents, sourceDir, targetRepoDir, diaryMode, apiUrl })
9894
10029
  });
9895
10030
  }
9896
10031
  //#endregion
10032
+ //#region src/commands/port.tsx
10033
+ var DIARY_MODES = [
10034
+ "new",
10035
+ "reuse",
10036
+ "skip"
10037
+ ];
10038
+ function assertDirectory(label, path) {
10039
+ try {
10040
+ if (!statSync(path).isDirectory()) throw new CliValidationError(`${label} "${path}" is not a directory`);
10041
+ } catch (err) {
10042
+ if (err instanceof CliValidationError) throw err;
10043
+ throw new CliValidationError(`${label} "${path}" does not exist`);
10044
+ }
10045
+ }
10046
+ var portCommand = defineCommand({
10047
+ meta: {
10048
+ name: "port",
10049
+ description: "Copy an existing agent from another repository into this one"
10050
+ },
10051
+ args: {
10052
+ name: {
10053
+ ...commonArgs.name,
10054
+ required: true
10055
+ },
10056
+ from: {
10057
+ type: "string",
10058
+ description: "Absolute path to the source agent directory (must be `<repo-root>/.moltnet/<agent-name>` and contain moltnet.json + gitconfig)",
10059
+ required: true,
10060
+ valueHint: "repo-root/.moltnet/agent-name"
10061
+ },
10062
+ agent: commonArgs.agent,
10063
+ "api-url": commonArgs["api-url"],
10064
+ dir: commonArgs.dir,
10065
+ diary: {
10066
+ type: "enum",
10067
+ description: "How to handle the diary in the new repo: `new` creates a fresh diary, `reuse` reuses the source diary ID, `skip` leaves MOLTNET_DIARY_ID unset",
10068
+ options: [...DIARY_MODES],
10069
+ default: "new",
10070
+ valueHint: "new|reuse|skip"
10071
+ }
10072
+ },
10073
+ run: withCleanErrors(({ args, rawArgs }) => {
10074
+ const name = requireAgentName(args.name);
10075
+ const agents = collectAgents(rawArgs);
10076
+ const apiUrl = resolveApiUrl(args["api-url"]);
10077
+ const dir = resolveDir(args.dir);
10078
+ const fromValidation = validatePortFromArg(args.from);
10079
+ if (!fromValidation.ok) throw new CliValidationError(fromValidation.error);
10080
+ const absoluteFromDir = args.from;
10081
+ assertDirectory("--dir", dir);
10082
+ assertDirectory("--from", absoluteFromDir);
10083
+ render(/* @__PURE__ */ jsx(PortApp, {
10084
+ name,
10085
+ agents: agents.length > 0 ? agents : ["claude"],
10086
+ sourceDir: absoluteFromDir,
10087
+ targetRepoDir: dir,
10088
+ diaryMode: args.diary,
10089
+ apiUrl
10090
+ }));
10091
+ })
10092
+ });
10093
+ //#endregion
9897
10094
  //#region src/SetupApp.tsx
9898
10095
  function SetupApp({ name, agents: agentsProp, apiUrl, dir }) {
9899
10096
  const { exit } = useApp();
@@ -10009,131 +10206,39 @@ function SetupApp({ name, agents: agentsProp, apiUrl, dir }) {
10009
10206
  }
10010
10207
  //#endregion
10011
10208
  //#region src/index.tsx
10012
- var rawArgs = process.argv.slice(2);
10013
- if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
10014
- const help = resolveHelpCommand(rawArgs, COMMANDS);
10015
- if (help) printCommandHelp(help);
10016
- else printRootHelp(COMMANDS);
10017
- process.exit(0);
10018
- }
10019
- if (rawArgs.length === 0) {
10020
- printRootHelp(COMMANDS);
10021
- process.exit(0);
10022
- }
10023
- var { values, positionals } = parseArgs({
10024
- args: rawArgs,
10025
- allowPositionals: true,
10026
- options: {
10027
- name: {
10028
- type: "string",
10029
- short: "n"
10030
- },
10031
- agent: {
10032
- type: "string",
10033
- short: "a",
10034
- multiple: true
10035
- },
10036
- "api-url": { type: "string" },
10037
- dir: { type: "string" },
10038
- org: {
10039
- type: "string",
10040
- short: "o"
10041
- },
10042
- from: { type: "string" },
10043
- diary: { type: "string" }
10209
+ runMain(defineCommand({
10210
+ meta: {
10211
+ name: "legreffier",
10212
+ description: "LeGreffier — attribution and measured memory for AI coding agents"
10213
+ },
10214
+ subCommands: {
10215
+ init: initCommand,
10216
+ setup: defineCommand({
10217
+ meta: {
10218
+ name: "setup",
10219
+ description: "Install LeGreffier skills and MCP config into an existing repo"
10220
+ },
10221
+ args: {
10222
+ name: {
10223
+ ...commonArgs.name,
10224
+ required: true
10225
+ },
10226
+ agent: commonArgs.agent,
10227
+ "api-url": commonArgs["api-url"],
10228
+ dir: commonArgs.dir
10229
+ },
10230
+ run: withCleanErrors(({ args, rawArgs }) => {
10231
+ render(/* @__PURE__ */ jsx(SetupApp, {
10232
+ name: requireAgentName(args.name),
10233
+ agents: collectAgents(rawArgs),
10234
+ apiUrl: resolveApiUrl(args["api-url"]),
10235
+ dir: resolveDir(args.dir)
10236
+ }));
10237
+ })
10238
+ }),
10239
+ port: portCommand,
10240
+ github: githubCommand
10044
10241
  }
10045
- });
10046
- var subcommand = positionals[0] ?? "init";
10047
- var name = values["name"];
10048
- var agentFlags = values["agent"] ?? [];
10049
- var apiUrl = values["api-url"] ?? process.env.MOLTNET_API_URL ?? "https://api.themolt.net";
10050
- var dir = values["dir"] ?? process.cwd();
10051
- var org = values["org"];
10052
- var fromDir = values["from"];
10053
- var diaryModeArg = values["diary"];
10054
- if (diaryModeArg !== void 0 && subcommand !== "port") {
10055
- process.stderr.write(`Error: --diary is only valid for \`legreffier port\` (got subcommand "${subcommand}")\n`);
10056
- process.exit(1);
10057
- }
10058
- if (subcommand === "github" && positionals[1] === "token") try {
10059
- printGitHubToken(resolveAgentName(name, process.env.GIT_CONFIG_GLOBAL), dir);
10060
- process.exit(0);
10061
- } catch (err) {
10062
- process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
10063
- process.exit(1);
10064
- }
10065
- if (!name) {
10066
- const help = COMMANDS.find((c) => c.command === subcommand);
10067
- process.stderr.write(`Error: --name is required.\n\nRun \`legreffier ${help ? help.command : "<command>"} --help\` for details.\n`);
10068
- process.exit(1);
10069
- }
10070
- if (!/^[a-z0-9][a-z0-9-]{0,37}[a-z0-9]$/.test(name)) {
10071
- process.stderr.write(`Invalid agent name: "${name}". Must be 2-39 lowercase alphanumeric characters or hyphens, starting and ending with a letter or digit.\n`);
10072
- process.exit(1);
10073
- }
10074
- for (const a of agentFlags) if (!SUPPORTED_AGENTS.includes(a)) {
10075
- process.stderr.write(`Unsupported agent: ${a}. Supported: ${SUPPORTED_AGENTS.join(", ")}\n`);
10076
- process.exit(1);
10077
- }
10078
- var agents = agentFlags;
10079
- if (subcommand === "setup") render(/* @__PURE__ */ jsx(SetupApp, {
10080
- name,
10081
- agents,
10082
- apiUrl,
10083
- dir
10084
- }));
10085
- else if (subcommand === "init") render(/* @__PURE__ */ jsx(InitApp, {
10086
- name,
10087
- agents: agents.length > 0 ? agents : void 0,
10088
- apiUrl,
10089
- dir,
10090
- org
10091
10242
  }));
10092
- else if (subcommand === "port") {
10093
- const fromValidation = validatePortFromArg(fromDir);
10094
- if (!fromValidation.ok) {
10095
- process.stderr.write(`Error: ${fromValidation.error}\n`);
10096
- process.exit(1);
10097
- }
10098
- const absoluteFromDir = fromDir;
10099
- const resolvedDiaryMode = diaryModeArg ?? "new";
10100
- if (![
10101
- "new",
10102
- "reuse",
10103
- "skip"
10104
- ].includes(resolvedDiaryMode)) {
10105
- process.stderr.write(`Error: --diary must be one of: new, reuse, skip (got "${resolvedDiaryMode}")\n`);
10106
- process.exit(1);
10107
- }
10108
- try {
10109
- if (!statSync(dir).isDirectory()) {
10110
- process.stderr.write(`Error: --dir "${dir}" is not a directory\n`);
10111
- process.exit(1);
10112
- }
10113
- } catch {
10114
- process.stderr.write(`Error: --dir "${dir}" does not exist\n`);
10115
- process.exit(1);
10116
- }
10117
- try {
10118
- if (!statSync(absoluteFromDir).isDirectory()) {
10119
- process.stderr.write(`Error: --from "${absoluteFromDir}" is not a directory\n`);
10120
- process.exit(1);
10121
- }
10122
- } catch {
10123
- process.stderr.write(`Error: --from "${absoluteFromDir}" does not exist\n`);
10124
- process.exit(1);
10125
- }
10126
- render(/* @__PURE__ */ jsx(PortApp, {
10127
- name,
10128
- agents: agents.length > 0 ? agents : ["claude"],
10129
- sourceDir: absoluteFromDir,
10130
- targetRepoDir: dir,
10131
- diaryMode: resolvedDiaryMode,
10132
- apiUrl
10133
- }));
10134
- } else {
10135
- process.stderr.write(`Unknown subcommand: ${subcommand}. Use "init", "setup", or "port".\n`);
10136
- process.exit(1);
10137
- }
10138
10243
  //#endregion
10139
10244
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/legreffier",
3
- "version": "0.31.0",
3
+ "version": "0.32.1",
4
4
  "description": "LeGreffier — attribution and measured memory for AI coding agents.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
@@ -20,24 +20,23 @@
20
20
  "node": ">=22"
21
21
  },
22
22
  "dependencies": {
23
- "figlet": "^1.8.0",
23
+ "citty": "^0.2.2",
24
24
  "ink": "^6.8.0",
25
25
  "open": "^10.1.2",
26
26
  "react": "^19.0.0",
27
27
  "smol-toml": "^1.6.1"
28
28
  },
29
29
  "devDependencies": {
30
- "@types/figlet": "^1.7.0",
31
30
  "@types/node": "^20.11.0",
32
31
  "@types/react": "^19.0.0",
33
32
  "typescript": "^5.3.3",
34
33
  "vite": "^8.0.0",
35
34
  "vitest": "^3.0.0",
36
35
  "@moltnet/api-client": "0.1.0",
37
- "@themoltnet/design-system": "0.4.0",
38
- "@themoltnet/github-agent": "0.23.0",
39
- "@themoltnet/sdk": "0.89.0",
40
- "@moltnet/crypto-service": "0.1.0"
36
+ "@moltnet/crypto-service": "0.1.0",
37
+ "@themoltnet/design-system": "0.5.1",
38
+ "@themoltnet/github-agent": "0.23.1",
39
+ "@themoltnet/sdk": "0.89.0"
41
40
  },
42
41
  "scripts": {
43
42
  "dev": "vite build --watch",