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/setup.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
|
+
import { mkdir, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
4
|
+
import { Me2lonAgentApi } from "./api.js";
|
|
5
|
+
import { parseBaseUrl, projectConfigPath, readConfigFile, writeProjectConfig } from "./config.js";
|
|
6
|
+
import { discoverWorkspace } from "./workspace-source.js";
|
|
7
|
+
import { Me2lonClientError } from "./errors.js";
|
|
8
|
+
import type { ConfigFileShape, SafeToolResult } from "./types.js";
|
|
9
|
+
|
|
10
|
+
export type SetupOptions = {
|
|
11
|
+
directory?: string | undefined;
|
|
12
|
+
baseUrl?: string | undefined;
|
|
13
|
+
configPath?: string | undefined;
|
|
14
|
+
projectName?: string | undefined;
|
|
15
|
+
newCredential?: boolean | undefined;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function stringField(value: Record<string, unknown>, key: string): string {
|
|
19
|
+
const field = value[key];
|
|
20
|
+
if (typeof field !== "string" || field.length === 0 || field.length > 4096 || /[\r\n\0]/u.test(field)) {
|
|
21
|
+
throw new Me2lonClientError("ME2LON_API_RESPONSE_INVALID", "The authorization response is incomplete.");
|
|
22
|
+
}
|
|
23
|
+
return field;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function setupProject(options: SetupOptions, runtime: {
|
|
27
|
+
fetch?: typeof fetch;
|
|
28
|
+
wait?: (milliseconds: number) => Promise<unknown>;
|
|
29
|
+
notify: (value: SafeToolResult) => void;
|
|
30
|
+
}): Promise<SafeToolResult> {
|
|
31
|
+
const workspace = await discoverWorkspace(options.directory ?? process.cwd());
|
|
32
|
+
const requestedPath = resolve(options.configPath ?? process.env.ME2LON_AGENT_CONFIG ?? projectConfigPath(workspace.root));
|
|
33
|
+
await mkdir(dirname(requestedPath), { recursive: true, mode: 0o700 });
|
|
34
|
+
const configPath = resolve(await realpath(dirname(requestedPath)), basename(requestedPath));
|
|
35
|
+
const relativeConfig = relative(workspace.root, configPath);
|
|
36
|
+
if (!relativeConfig.startsWith(`..${sep}`) && relativeConfig !== ".." && !isAbsolute(relativeConfig)) {
|
|
37
|
+
throw new Me2lonClientError("ME2LON_CONFIG_IN_SOURCE", "Keep the setup configuration outside the uploaded repository.");
|
|
38
|
+
}
|
|
39
|
+
let previous: ConfigFileShape = {};
|
|
40
|
+
try { await stat(configPath); previous = await readConfigFile(configPath); }
|
|
41
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
|
|
42
|
+
if (previous.workspaceRoot && previous.workspaceRoot !== workspace.root) {
|
|
43
|
+
throw new Me2lonClientError("ME2LON_CONFIG_INVALID", "This configuration belongs to a different local project.");
|
|
44
|
+
}
|
|
45
|
+
const baseUrl = parseBaseUrl(options.baseUrl ?? process.env.ME2LON_AGENT_URL ?? previous.baseUrl ?? "https://me2lon.clarus.me");
|
|
46
|
+
const sameProject = previous.sourceFormat === "snapshot" && previous.baseUrl === baseUrl.origin;
|
|
47
|
+
const api = new Me2lonAgentApi(baseUrl, "", undefined, runtime.fetch);
|
|
48
|
+
const authorization = await api.request("/device-authorization", { method: "POST", body: {
|
|
49
|
+
projectName: options.projectName ?? basename(workspace.root).slice(0, 80),
|
|
50
|
+
...(sameProject && previous.repositoryId ? { repositoryId: previous.repositoryId } : {}),
|
|
51
|
+
...(sameProject && previous.credentialId && !options.newCredential ? { credentialId: previous.credentialId } : {}),
|
|
52
|
+
} });
|
|
53
|
+
const deviceCode = stringField(authorization, "deviceCode");
|
|
54
|
+
const userCode = stringField(authorization, "userCode");
|
|
55
|
+
const verificationUri = stringField(authorization, "verificationUri");
|
|
56
|
+
const verificationUrl = new URL(verificationUri);
|
|
57
|
+
parseBaseUrl(verificationUrl.origin);
|
|
58
|
+
if (verificationUrl.pathname !== "/agent-setup" || verificationUrl.searchParams.get("code") !== userCode ||
|
|
59
|
+
[...verificationUrl.searchParams.keys()].some((key) => key !== "code")) {
|
|
60
|
+
throw new Me2lonClientError("ME2LON_API_RESPONSE_INVALID", "The authorization link is invalid.");
|
|
61
|
+
}
|
|
62
|
+
runtime.notify({ status: "authorization_required", verificationUri, userCode,
|
|
63
|
+
message: "Open the link, sign in, and confirm that the displayed code matches. No token needs to be copied." });
|
|
64
|
+
const timeout = Math.min(600, typeof authorization.expiresIn === "number" ? authorization.expiresIn : 600) * 1000;
|
|
65
|
+
const deadline = Date.now() + timeout;
|
|
66
|
+
let interval = 5_000;
|
|
67
|
+
while (Date.now() < deadline) {
|
|
68
|
+
await (runtime.wait ?? delay)(interval);
|
|
69
|
+
const result = await api.request("/device-authorization/token", { method: "POST", body: { deviceCode } });
|
|
70
|
+
if (result.status === "pending") continue;
|
|
71
|
+
if (result.status === "slow_down") { interval = Math.min(30_000, interval + 5_000); continue; }
|
|
72
|
+
if (result.status === "denied" || result.status === "expired") {
|
|
73
|
+
throw new Me2lonClientError("ME2LON_AUTHORIZATION_INCOMPLETE", `Authorization ${result.status}; run setup again when ready.`);
|
|
74
|
+
}
|
|
75
|
+
if (result.status !== "authorized" || typeof result.credential !== "object" || result.credential === null) {
|
|
76
|
+
throw new Me2lonClientError("ME2LON_API_RESPONSE_INVALID", "Unknown authorization response.");
|
|
77
|
+
}
|
|
78
|
+
const token = stringField(result, "token");
|
|
79
|
+
if (!/^m2lon_agent_v1_[A-Za-z0-9_-]{43}$/u.test(token)) throw new Me2lonClientError("ME2LON_API_RESPONSE_INVALID", "Invalid authorization credential.");
|
|
80
|
+
const credentialId = stringField(result.credential as Record<string, unknown>, "id");
|
|
81
|
+
const repositoryId = stringField(result, "repositoryId");
|
|
82
|
+
const keepState = sameProject && previous.credentialId === credentialId && previous.repositoryId === repositoryId;
|
|
83
|
+
await writeProjectConfig(configPath, {
|
|
84
|
+
baseUrl: baseUrl.origin, token, workspaceRoot: workspace.root, repositoryId,
|
|
85
|
+
credentialId, sourceFormat: "snapshot",
|
|
86
|
+
...(keepState && previous.workspaceId ? { workspaceId: previous.workspaceId } : {}),
|
|
87
|
+
...(keepState && previous.previousRevisionId ? { previousRevisionId: previous.previousRevisionId } : {}),
|
|
88
|
+
});
|
|
89
|
+
return {
|
|
90
|
+
status: "ready", repositoryId, configPath,
|
|
91
|
+
nextCommand: "me2lon analyze --wait",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
throw new Me2lonClientError("ME2LON_AUTHORIZATION_INCOMPLETE", "Authorization expired; run setup again.");
|
|
95
|
+
}
|
package/src/skill.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Me2lonClientError } from "./errors.js";
|
|
2
|
+
import { mkdir, readFile, lstat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export async function installSkill(agent = 'generic', destination?: string, force = false, home = homedir()): Promise<string> {
|
|
7
|
+
if (!['generic', 'codex', 'claude'].includes(agent)) throw new Me2lonClientError("ME2LON_CLIENT_INPUT", 'Use --agent generic, codex, or claude, or --path DIRECTORY.');
|
|
8
|
+
const target = destination ? resolve(destination) : join(home, agent === 'claude' ? '.claude' : '.agents', 'skills', 'me2lon');
|
|
9
|
+
const files = await Promise.all(['SKILL.md', 'LICENSE'].map(async name => ({ name, text: await readFile(new URL(`../skill/${name}`, import.meta.url), 'utf8') })));
|
|
10
|
+
// Check every destination before changing any file. Never follow destination file symlinks.
|
|
11
|
+
for (const { name, text } of files) {
|
|
12
|
+
const path = join(target, name);
|
|
13
|
+
try {
|
|
14
|
+
const stat = await lstat(path);
|
|
15
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Me2lonClientError("ME2LON_CLIENT_INPUT", `Refusing non-regular skill file: ${path}`);
|
|
16
|
+
if (!force && await readFile(path, 'utf8') !== text) throw new Me2lonClientError("ME2LON_CLIENT_INPUT", `Existing skill differs: ${path}. Use --force to replace it.`);
|
|
17
|
+
} catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
|
|
18
|
+
}
|
|
19
|
+
await mkdir(target, { recursive: true });
|
|
20
|
+
for (const { name, text } of files) await writeFile(join(target, name), text);
|
|
21
|
+
return target;
|
|
22
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export const SOURCE_SCHEMA_VERSION = "me2lon-agent-source/v1" as const;
|
|
2
|
+
export const SNAPSHOT_SCHEMA_VERSION = "me2lon-agent-source/v2" as const;
|
|
3
|
+
|
|
4
|
+
export const LOCAL_SOURCE_LIMITS = Object.freeze({
|
|
5
|
+
maxChanges: 2_000,
|
|
6
|
+
maxFileBytes: 1024 * 1024,
|
|
7
|
+
maxTotalBytes: 8 * 1024 * 1024,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
11
|
+
export type JsonValue =
|
|
12
|
+
| JsonPrimitive
|
|
13
|
+
| JsonValue[]
|
|
14
|
+
| { [key: string]: JsonValue };
|
|
15
|
+
|
|
16
|
+
export type ReplaceChange = {
|
|
17
|
+
operation: "replace";
|
|
18
|
+
path: string;
|
|
19
|
+
content: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type DeleteChange = {
|
|
23
|
+
operation: "delete";
|
|
24
|
+
path: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type SourceChange = ReplaceChange | DeleteChange;
|
|
28
|
+
|
|
29
|
+
export type SourceLimits = {
|
|
30
|
+
maxChanges: number;
|
|
31
|
+
maxFileBytes: number;
|
|
32
|
+
maxTotalBytes: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type WorkspaceSource = {
|
|
36
|
+
schemaVersion: typeof SOURCE_SCHEMA_VERSION;
|
|
37
|
+
baseCommitSha: string;
|
|
38
|
+
changes: SourceChange[];
|
|
39
|
+
digest: string;
|
|
40
|
+
replacementCount: number;
|
|
41
|
+
deletionCount: number;
|
|
42
|
+
totalReplacementBytes: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type BoundWorkspace = {
|
|
46
|
+
root: string;
|
|
47
|
+
repositoryId: string;
|
|
48
|
+
baseCommitSha: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type AgentRuntimeConfig = {
|
|
52
|
+
baseUrl: URL;
|
|
53
|
+
token: string;
|
|
54
|
+
workspace: BoundWorkspace;
|
|
55
|
+
configPath?: string;
|
|
56
|
+
sourceFormat?: "snapshot";
|
|
57
|
+
credentialId?: string;
|
|
58
|
+
workspaceId?: string;
|
|
59
|
+
previousRevisionId?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type ConfigFileShape = {
|
|
63
|
+
baseUrl?: string;
|
|
64
|
+
token?: string;
|
|
65
|
+
workspaceRoot?: string;
|
|
66
|
+
repositoryId?: string;
|
|
67
|
+
baseCommitSha?: string;
|
|
68
|
+
sourceFormat?: "snapshot";
|
|
69
|
+
credentialId?: string;
|
|
70
|
+
workspaceId?: string;
|
|
71
|
+
previousRevisionId?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type WorkspaceSnapshot = {
|
|
75
|
+
schemaVersion: typeof SNAPSHOT_SCHEMA_VERSION;
|
|
76
|
+
baseCommitSha: string;
|
|
77
|
+
branchLabel: string | null;
|
|
78
|
+
files: { path: string; content: string }[];
|
|
79
|
+
digest: string;
|
|
80
|
+
totalReplacementBytes: number;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export type SafeToolResult = {
|
|
84
|
+
[key: string]: JsonValue;
|
|
85
|
+
}
|