me2lon 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +62 -2
- package/dist/api.d.ts +35 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +286 -0
- package/dist/api.js.map +1 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +261 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +23 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +272 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +173 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +31 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/presentation.d.ts +12 -0
- package/dist/presentation.d.ts.map +1 -0
- package/dist/presentation.js +50 -0
- package/dist/presentation.js.map +1 -0
- package/dist/safe-output.d.ts +20 -0
- package/dist/safe-output.d.ts.map +1 -0
- package/dist/safe-output.js +770 -0
- package/dist/safe-output.js.map +1 -0
- package/dist/setup.d.ts +14 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +92 -0
- package/dist/setup.js.map +1 -0
- package/dist/skill.d.ts +2 -0
- package/dist/skill.d.ts.map +1 -0
- package/dist/skill.js +30 -0
- package/dist/skill.js.map +1 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/workspace-source.d.ts +11 -0
- package/dist/workspace-source.d.ts.map +1 -0
- package/dist/workspace-source.js +387 -0
- package/dist/workspace-source.js.map +1 -0
- package/package.json +43 -4
- package/skill/LICENSE +21 -0
- package/skill/SKILL.md +18 -0
- package/src/api.ts +426 -0
- package/src/cli.ts +305 -0
- package/src/client.ts +413 -0
- package/src/config.ts +252 -0
- package/src/errors.ts +40 -0
- package/src/index.ts +37 -0
- package/src/presentation.ts +39 -0
- package/src/safe-output.ts +826 -0
- package/src/setup.ts +95 -0
- package/src/skill.ts +22 -0
- package/src/types.ts +85 -0
- package/src/workspace-source.ts +526 -0
- package/tsconfig.build.json +18 -0
- package/tsconfig.json +14 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
|
|
7
|
+
import { Me2lonAgentClient } from "./client.js";
|
|
8
|
+
import { resolveRuntimeConfig } from "./config.js";
|
|
9
|
+
import { Me2lonClientError, publicError } from "./errors.js";
|
|
10
|
+
import { installSkill } from "./skill.js";
|
|
11
|
+
import { humanOutput, waitForJob } from "./presentation.js";
|
|
12
|
+
import { setupProject } from "./setup.js";
|
|
13
|
+
import type { JsonValue } from "./types.js";
|
|
14
|
+
|
|
15
|
+
const HELP = `me2lon — CLI client for /api/agent/v1
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
me2lon setup [--base-url URL] [--project-name NAME] [--new-credential]
|
|
19
|
+
me2lon skill install [--agent generic|codex|claude] [--path DIRECTORY] [--force]
|
|
20
|
+
--json machine-readable output
|
|
21
|
+
--wait wait for analyze/get-job (default timeout: 7200 seconds)
|
|
22
|
+
--timeout-seconds N override waiting timeout
|
|
23
|
+
--version print client version
|
|
24
|
+
me2lon capabilities [connection options]
|
|
25
|
+
me2lon analyze [--project-root PATH] [--revision-id ID] [connection options]
|
|
26
|
+
me2lon check-workspace [connection options]
|
|
27
|
+
me2lon get-job --job-id ID [connection options]
|
|
28
|
+
me2lon translate --check-job-id ID --project-id ID [connection options]
|
|
29
|
+
me2lon verify --translation-job-id ID --expected-policy-set-digest DIGEST [connection options]
|
|
30
|
+
me2lon get-policy [--policy-unit-id ID] [connection options]
|
|
31
|
+
me2lon propose-policy --policy-unit-id ID --expected-policy-set-digest DIGEST --policy-file FILE [--rationale TEXT] [connection options]
|
|
32
|
+
me2lon cancel-job --job-id ID [connection options]
|
|
33
|
+
|
|
34
|
+
Connection options:
|
|
35
|
+
--base-url URL API origin (or ME2LON_AGENT_URL)
|
|
36
|
+
--workspace-root PATH exact Git root (or ME2LON_WORKSPACE_ROOT; default cwd)
|
|
37
|
+
--repository-id ID bound repository (or ME2LON_REPOSITORY_ID)
|
|
38
|
+
--base-commit SHA full 40-character commit (or ME2LON_BASE_COMMIT)
|
|
39
|
+
--config FILE mode-0600 JSON config (or ME2LON_AGENT_CONFIG)
|
|
40
|
+
--token-stdin read the token from stdin
|
|
41
|
+
|
|
42
|
+
The token is accepted only from ME2LON_AGENT_TOKEN, a protected config file,
|
|
43
|
+
or stdin for one-shot CLI calls. A token option is deliberately not supported.
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
const options = {
|
|
47
|
+
json: { type: "boolean" as const },
|
|
48
|
+
wait: { type: "boolean" as const },
|
|
49
|
+
version: { type: "boolean" as const },
|
|
50
|
+
"timeout-seconds": { type: "string" as const },
|
|
51
|
+
agent: { type: "string" as const },
|
|
52
|
+
path: { type: "string" as const },
|
|
53
|
+
force: { type: "boolean" as const },
|
|
54
|
+
"project-name": { type: "string" as const },
|
|
55
|
+
"new-credential": { type: "boolean" as const, default: false },
|
|
56
|
+
"base-url": { type: "string" as const },
|
|
57
|
+
"workspace-root": { type: "string" as const },
|
|
58
|
+
"repository-id": { type: "string" as const },
|
|
59
|
+
"base-commit": { type: "string" as const },
|
|
60
|
+
config: { type: "string" as const },
|
|
61
|
+
"token-stdin": { type: "boolean" as const, default: false },
|
|
62
|
+
"job-id": { type: "string" as const },
|
|
63
|
+
"check-job-id": { type: "string" as const },
|
|
64
|
+
"translation-job-id": { type: "string" as const },
|
|
65
|
+
"project-root": { type: "string" as const },
|
|
66
|
+
"revision-id": { type: "string" as const },
|
|
67
|
+
"project-id": { type: "string" as const },
|
|
68
|
+
"policy-unit-id": { type: "string" as const },
|
|
69
|
+
"expected-policy-set-digest": { type: "string" as const },
|
|
70
|
+
"policy-file": { type: "string" as const },
|
|
71
|
+
rationale: { type: "string" as const },
|
|
72
|
+
help: { type: "boolean" as const, short: "h", default: false },
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function rejectSecretArguments(argv: string[]): void {
|
|
76
|
+
if (
|
|
77
|
+
argv.some(
|
|
78
|
+
(argument) =>
|
|
79
|
+
argument === "--token" ||
|
|
80
|
+
argument.startsWith("--token=") ||
|
|
81
|
+
argument === "--api-key" ||
|
|
82
|
+
argument.startsWith("--api-key="),
|
|
83
|
+
)
|
|
84
|
+
) {
|
|
85
|
+
throw new Me2lonClientError(
|
|
86
|
+
"ME2LON_TOKEN_ON_ARGV",
|
|
87
|
+
"Me2lon tokens are never accepted on the command line. Use ME2LON_AGENT_TOKEN, a protected config file, or --token-stdin.",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function tokenFromStdin(): Promise<string> {
|
|
93
|
+
const chunks: Buffer[] = [];
|
|
94
|
+
let bytes = 0;
|
|
95
|
+
for await (const chunk of process.stdin) {
|
|
96
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
97
|
+
bytes += buffer.length;
|
|
98
|
+
if (bytes > 8_192) {
|
|
99
|
+
throw new Me2lonClientError(
|
|
100
|
+
"ME2LON_TOKEN_INVALID",
|
|
101
|
+
"The stdin token exceeds the Me2lon client safety limit.",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
chunks.push(buffer);
|
|
105
|
+
}
|
|
106
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function requiredOption(
|
|
110
|
+
values: Record<string, string | boolean | undefined>,
|
|
111
|
+
name: string,
|
|
112
|
+
): string {
|
|
113
|
+
const value = values[name];
|
|
114
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
115
|
+
throw new Me2lonClientError(
|
|
116
|
+
"ME2LON_CLI_INPUT_MISSING",
|
|
117
|
+
`Missing --${name}.`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return value.trim();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function readPolicy(path: string): Promise<JsonValue> {
|
|
124
|
+
const bytes = await readFile(path);
|
|
125
|
+
if (bytes.length > 1024 * 1024) {
|
|
126
|
+
throw new Me2lonClientError(
|
|
127
|
+
"ME2LON_POLICY_FILE_LIMIT",
|
|
128
|
+
"The policy proposal exceeds the 1 MiB client limit.",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) as JsonValue;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
throw new Me2lonClientError(
|
|
135
|
+
"ME2LON_POLICY_FILE_INVALID",
|
|
136
|
+
"The policy proposal file must contain valid UTF-8 JSON.",
|
|
137
|
+
{ cause: error },
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function runCli(argv: string[]): Promise<number> {
|
|
143
|
+
rejectSecretArguments(argv);
|
|
144
|
+
const parsed = parseArgs({
|
|
145
|
+
args: argv,
|
|
146
|
+
options,
|
|
147
|
+
allowPositionals: true,
|
|
148
|
+
strict: true,
|
|
149
|
+
});
|
|
150
|
+
const [command, ...extraPositionals] = parsed.positionals;
|
|
151
|
+
const print = (value: JsonValue) => process.stdout.write(`${parsed.values.json ? JSON.stringify(value) : humanOutput(value)}\n`);
|
|
152
|
+
if (parsed.values.version) {
|
|
153
|
+
const { version } = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
154
|
+
print(parsed.values.json ? { version } : version); return 0;
|
|
155
|
+
}
|
|
156
|
+
if (command === 'skill' && extraPositionals.length === 1 && extraPositionals[0] === 'install') {
|
|
157
|
+
const path = await installSkill(parsed.values.agent, parsed.values.path, parsed.values.force);
|
|
158
|
+
print({ installed: path }); return 0;
|
|
159
|
+
}
|
|
160
|
+
if (parsed.values.wait && !['analyze', 'get-job'].includes(command ?? '')) throw new Me2lonClientError("ME2LON_CLI_INPUT_INVALID", '--wait requires analyze or get-job.');
|
|
161
|
+
const timeout = Number(parsed.values['timeout-seconds'] ?? 7200);
|
|
162
|
+
if (!Number.isFinite(timeout) || timeout <= 0) throw new Me2lonClientError("ME2LON_CLI_INPUT_INVALID", '--timeout-seconds must be a positive number.');
|
|
163
|
+
if (parsed.values.help || !command) {
|
|
164
|
+
process.stdout.write(HELP);
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
if (extraPositionals.length > 0) {
|
|
168
|
+
throw new Me2lonClientError(
|
|
169
|
+
"ME2LON_CLI_INPUT_INVALID",
|
|
170
|
+
"Unexpected positional command arguments.",
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
if (command === "setup") {
|
|
174
|
+
const result = await setupProject({
|
|
175
|
+
directory: parsed.values["workspace-root"], baseUrl: parsed.values["base-url"],
|
|
176
|
+
configPath: parsed.values.config, projectName: parsed.values["project-name"],
|
|
177
|
+
newCredential: parsed.values["new-credential"],
|
|
178
|
+
}, { notify: (value) => process.stderr.write(`${parsed.values.json ? JSON.stringify(value) : humanOutput(value)}\n`) });
|
|
179
|
+
print(result);
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
const stdinToken = parsed.values["token-stdin"]
|
|
183
|
+
? await tokenFromStdin()
|
|
184
|
+
: undefined;
|
|
185
|
+
const config = await resolveRuntimeConfig({
|
|
186
|
+
...(parsed.values["base-url"]
|
|
187
|
+
? { baseUrl: parsed.values["base-url"] }
|
|
188
|
+
: {}),
|
|
189
|
+
...(parsed.values["workspace-root"]
|
|
190
|
+
? { workspaceRoot: parsed.values["workspace-root"] }
|
|
191
|
+
: {}),
|
|
192
|
+
...(parsed.values["repository-id"]
|
|
193
|
+
? { repositoryId: parsed.values["repository-id"] }
|
|
194
|
+
: {}),
|
|
195
|
+
...(parsed.values["base-commit"]
|
|
196
|
+
? { baseCommitSha: parsed.values["base-commit"] }
|
|
197
|
+
: {}),
|
|
198
|
+
...(parsed.values.config ? { configPath: parsed.values.config } : {}),
|
|
199
|
+
...(stdinToken ? { tokenFromStdin: stdinToken } : {}),
|
|
200
|
+
});
|
|
201
|
+
const client = new Me2lonAgentClient(config);
|
|
202
|
+
let result;
|
|
203
|
+
switch (command) {
|
|
204
|
+
case "capabilities":
|
|
205
|
+
result = await client.capabilities();
|
|
206
|
+
break;
|
|
207
|
+
case "analyze":
|
|
208
|
+
result = await client.analyzeWorkspace(parsed.values["project-root"], parsed.values["revision-id"]);
|
|
209
|
+
break;
|
|
210
|
+
case "check-workspace":
|
|
211
|
+
result = await client.checkWorkspace();
|
|
212
|
+
break;
|
|
213
|
+
case "get-job":
|
|
214
|
+
result = await client.getJob(requiredOption(parsed.values, "job-id"));
|
|
215
|
+
break;
|
|
216
|
+
case "translate":
|
|
217
|
+
result = await client.translate(
|
|
218
|
+
requiredOption(parsed.values, "check-job-id"),
|
|
219
|
+
requiredOption(parsed.values, "project-id"),
|
|
220
|
+
);
|
|
221
|
+
break;
|
|
222
|
+
case "verify":
|
|
223
|
+
result = await client.verify(
|
|
224
|
+
requiredOption(parsed.values, "translation-job-id"),
|
|
225
|
+
requiredOption(parsed.values, "expected-policy-set-digest"),
|
|
226
|
+
);
|
|
227
|
+
break;
|
|
228
|
+
case "get-policy":
|
|
229
|
+
result = await client.getPolicy(
|
|
230
|
+
typeof parsed.values["policy-unit-id"] === "string"
|
|
231
|
+
? parsed.values["policy-unit-id"]
|
|
232
|
+
: undefined,
|
|
233
|
+
);
|
|
234
|
+
break;
|
|
235
|
+
case "propose-policy": {
|
|
236
|
+
const rationale = parsed.values.rationale;
|
|
237
|
+
result = await client.proposePolicy({
|
|
238
|
+
policyUnitId: requiredOption(parsed.values, "policy-unit-id"),
|
|
239
|
+
expectedPolicySetDigest: requiredOption(
|
|
240
|
+
parsed.values,
|
|
241
|
+
"expected-policy-set-digest",
|
|
242
|
+
),
|
|
243
|
+
policy: await readPolicy(requiredOption(parsed.values, "policy-file")),
|
|
244
|
+
...(typeof rationale === "string" ? { rationale } : {}),
|
|
245
|
+
});
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
case "cancel-job":
|
|
249
|
+
result = await client.cancelJob(requiredOption(parsed.values, "job-id"));
|
|
250
|
+
break;
|
|
251
|
+
default:
|
|
252
|
+
throw new Me2lonClientError(
|
|
253
|
+
"ME2LON_CLI_COMMAND_UNKNOWN",
|
|
254
|
+
`Unknown Me2lon command: ${command}.`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
if (parsed.values.wait) {
|
|
258
|
+
const initial = command === 'analyze' ? result.job : result;
|
|
259
|
+
if (!initial || typeof initial !== 'object' || Array.isArray(initial)) throw new Me2lonClientError("ME2LON_CLI_INPUT_INVALID", 'Missing job response.');
|
|
260
|
+
const controller = new AbortController();
|
|
261
|
+
const interrupt = () => controller.abort();
|
|
262
|
+
process.once('SIGINT', interrupt);
|
|
263
|
+
try {
|
|
264
|
+
const completed = await waitForJob(initial, id => client.getJob(id), {
|
|
265
|
+
timeoutMs: timeout * 1000, signal: controller.signal,
|
|
266
|
+
notify: message => process.stderr.write(`${message}\n`),
|
|
267
|
+
});
|
|
268
|
+
print(completed.result); return completed.exitCode;
|
|
269
|
+
} finally { process.removeListener('SIGINT', interrupt); }
|
|
270
|
+
}
|
|
271
|
+
print(result);
|
|
272
|
+
return 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function main(): Promise<void> {
|
|
276
|
+
try {
|
|
277
|
+
process.exitCode = await runCli(process.argv.slice(2));
|
|
278
|
+
} catch (error) {
|
|
279
|
+
const safe = publicError(error);
|
|
280
|
+
process.stderr.write(`${process.argv.includes("--json") ? JSON.stringify(safe) : safe.message}\n`);
|
|
281
|
+
process.exitCode = 1;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export async function isMainModule(
|
|
286
|
+
moduleUrl: string,
|
|
287
|
+
entrypoint: string | undefined,
|
|
288
|
+
): Promise<boolean> {
|
|
289
|
+
if (!entrypoint) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
const [modulePath, entrypointPath] = await Promise.all([
|
|
294
|
+
realpath(fileURLToPath(moduleUrl)),
|
|
295
|
+
realpath(entrypoint),
|
|
296
|
+
]);
|
|
297
|
+
return modulePath === entrypointPath;
|
|
298
|
+
} catch {
|
|
299
|
+
return moduleUrl === pathToFileURL(entrypoint).href;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (await isMainModule(import.meta.url, process.argv[1])) {
|
|
304
|
+
await main();
|
|
305
|
+
}
|