@themoltnet/legreffier 0.29.1 → 0.31.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 (2) hide show
  1. package/dist/index.js +330 -10
  2. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { statSync } from "node:fs";
3
3
  import { parseArgs, parseEnv } from "node:util";
4
4
  import { Box, Text, render, useApp, useInput } from "ink";
5
5
  import { execFileSync, execSync } from "node:child_process";
6
- import { basename, dirname, join } from "node:path";
6
+ import { basename, dirname, isAbsolute, join } from "node:path";
7
7
  import { useEffect, useReducer, useRef, useState } from "react";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
9
  import figlet from "figlet";
@@ -13,6 +13,251 @@ import { access, chmod, copyFile, mkdir, readFile, rm, writeFile } from "node:fs
13
13
  import { homedir } from "node:os";
14
14
  import { parse, stringify } from "smol-toml";
15
15
  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
16
261
  //#region src/github-token.ts
17
262
  var MOLTNET_GITCONFIG_RE = /\.moltnet\/([^/]+)\/gitconfig$/;
18
263
  function resolveAgentName(nameFlag, gitConfigGlobal) {
@@ -596,6 +841,32 @@ function CliSummaryBox({ agentName, fingerprint, appSlug, apiUrl, mcpUrl }) {
596
841
  }),
597
842
  " in any repo where the app is installed."
598
843
  ]
844
+ }),
845
+ /* @__PURE__ */ jsx(Text, { children: " " }),
846
+ /* @__PURE__ */ jsxs(Text, {
847
+ color: cliTheme.color.text,
848
+ children: [
849
+ " ",
850
+ /* @__PURE__ */ jsx(Text, {
851
+ color: cliTheme.color.primary,
852
+ children: "Next step:"
853
+ }),
854
+ " run ",
855
+ /* @__PURE__ */ jsx(Text, {
856
+ color: cliTheme.color.accent,
857
+ children: "/legreffier-onboarding"
858
+ }),
859
+ " or ",
860
+ /* @__PURE__ */ jsx(Text, {
861
+ color: cliTheme.color.accent,
862
+ children: "$legreffier-onboarding"
863
+ }),
864
+ " in your next session"
865
+ ]
866
+ }),
867
+ /* @__PURE__ */ jsxs(Text, {
868
+ color: cliTheme.color.muted,
869
+ children: [" ", "to connect your team diary and start capturing knowledge."]
599
870
  })
600
871
  ]
601
872
  })
@@ -7485,6 +7756,10 @@ var SKILLS = [
7485
7756
  {
7486
7757
  name: "legreffier-explore",
7487
7758
  files: ["SKILL.md", "references/exploration-pack-plan.yaml"]
7759
+ },
7760
+ {
7761
+ name: "legreffier-onboarding",
7762
+ files: ["SKILL.md"]
7488
7763
  }
7489
7764
  ];
7490
7765
  async function downloadSkillFiles(skill) {
@@ -9013,6 +9288,38 @@ function InitApp({ name, agents: agentsProp, apiUrl, dir = process.cwd(), org })
9013
9288
  });
9014
9289
  }
9015
9290
  //#endregion
9291
+ //#region src/phases/portArgs.ts
9292
+ /**
9293
+ * Validate the raw `--from` argument passed to `legreffier port` before
9294
+ * any filesystem access.
9295
+ *
9296
+ * `--from` must be:
9297
+ * - non-empty
9298
+ * - an absolute path (no `~`, no relative paths, no bare repo names)
9299
+ *
9300
+ * The help text for `port` documents this as a hard requirement because
9301
+ * the port pipeline rewrites paths embedded in `moltnet.json` and
9302
+ * `gitconfig`, and those rewrites only round-trip correctly when the
9303
+ * source is an absolute path. Accepting a relative path here silently
9304
+ * produces broken output in git worktrees (different CWD than the main
9305
+ * worktree root), so we fail fast instead of letting the port run.
9306
+ */
9307
+ function validatePortFromArg(fromDir) {
9308
+ if (typeof fromDir !== "string" || fromDir.length === 0) return {
9309
+ ok: false,
9310
+ error: "legreffier port requires --from <repo-root>/.moltnet/<agent>"
9311
+ };
9312
+ if (fromDir.startsWith("~")) return {
9313
+ ok: false,
9314
+ error: `--from "${fromDir}" uses "~" which is not expanded. Pass an absolute path (e.g. "\$HOME/code/other-repo/.moltnet/<agent>").`
9315
+ };
9316
+ if (!isAbsolute(fromDir)) return {
9317
+ ok: false,
9318
+ error: `--from "${fromDir}" must be an absolute path (e.g. /Users/me/code/other-repo/.moltnet/<agent>). Relative paths break inside git worktrees where the CWD differs from the main worktree root.`
9319
+ };
9320
+ return { ok: true };
9321
+ }
9322
+ //#endregion
9016
9323
  //#region src/phases/portValidate.ts
9017
9324
  /**
9018
9325
  * Validate a source `.moltnet/<agent>/` directory for porting.
@@ -9702,8 +10009,19 @@ function SetupApp({ name, agents: agentsProp, apiUrl, dir }) {
9702
10009
  }
9703
10010
  //#endregion
9704
10011
  //#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
+ }
9705
10023
  var { values, positionals } = parseArgs({
9706
- args: process.argv.slice(2),
10024
+ args: rawArgs,
9707
10025
  allowPositionals: true,
9708
10026
  options: {
9709
10027
  name: {
@@ -9745,8 +10063,8 @@ if (subcommand === "github" && positionals[1] === "token") try {
9745
10063
  process.exit(1);
9746
10064
  }
9747
10065
  if (!name) {
9748
- const usage = subcommand === "setup" ? "Usage: legreffier setup --name <agent-name> [--agent claude] [--agent codex] [--dir <path>]" : subcommand === "port" ? "Usage: legreffier port --name <agent-name> --from <path/to/source/.moltnet/<agent>> [--agent claude] [--agent codex] [--dir <target-repo>] [--diary new|reuse|skip]" : "Usage: legreffier [init] --name <agent-name> [--agent claude] [--agent codex] [--api-url <url>] [--dir <path>] [--org <github-org>]";
9749
- process.stderr.write(usage + "\n");
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`);
9750
10068
  process.exit(1);
9751
10069
  }
9752
10070
  if (!/^[a-z0-9][a-z0-9-]{0,37}[a-z0-9]$/.test(name)) {
@@ -9772,10 +10090,12 @@ else if (subcommand === "init") render(/* @__PURE__ */ jsx(InitApp, {
9772
10090
  org
9773
10091
  }));
9774
10092
  else if (subcommand === "port") {
9775
- if (!fromDir) {
9776
- process.stderr.write("Error: legreffier port requires --from <path/to/source/.moltnet/<agent>>\n");
10093
+ const fromValidation = validatePortFromArg(fromDir);
10094
+ if (!fromValidation.ok) {
10095
+ process.stderr.write(`Error: ${fromValidation.error}\n`);
9777
10096
  process.exit(1);
9778
10097
  }
10098
+ const absoluteFromDir = fromDir;
9779
10099
  const resolvedDiaryMode = diaryModeArg ?? "new";
9780
10100
  if (![
9781
10101
  "new",
@@ -9795,18 +10115,18 @@ else if (subcommand === "port") {
9795
10115
  process.exit(1);
9796
10116
  }
9797
10117
  try {
9798
- if (!statSync(fromDir).isDirectory()) {
9799
- process.stderr.write(`Error: --from "${fromDir}" is not a directory\n`);
10118
+ if (!statSync(absoluteFromDir).isDirectory()) {
10119
+ process.stderr.write(`Error: --from "${absoluteFromDir}" is not a directory\n`);
9800
10120
  process.exit(1);
9801
10121
  }
9802
10122
  } catch {
9803
- process.stderr.write(`Error: --from "${fromDir}" does not exist\n`);
10123
+ process.stderr.write(`Error: --from "${absoluteFromDir}" does not exist\n`);
9804
10124
  process.exit(1);
9805
10125
  }
9806
10126
  render(/* @__PURE__ */ jsx(PortApp, {
9807
10127
  name,
9808
10128
  agents: agents.length > 0 ? agents : ["claude"],
9809
- sourceDir: fromDir,
10129
+ sourceDir: absoluteFromDir,
9810
10130
  targetRepoDir: dir,
9811
10131
  diaryMode: resolvedDiaryMode,
9812
10132
  apiUrl
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/legreffier",
3
- "version": "0.29.1",
3
+ "version": "0.31.0",
4
4
  "description": "LeGreffier — attribution and measured memory for AI coding agents.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
@@ -34,10 +34,10 @@
34
34
  "vite": "^8.0.0",
35
35
  "vitest": "^3.0.0",
36
36
  "@moltnet/api-client": "0.1.0",
37
- "@themoltnet/design-system": "0.3.2",
38
- "@moltnet/crypto-service": "0.1.0",
39
- "@themoltnet/sdk": "0.88.0",
40
- "@themoltnet/github-agent": "0.23.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"
41
41
  },
42
42
  "scripts": {
43
43
  "dev": "vite build --watch",