@ainecto/cli 0.9.0 → 0.10.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 +10 -1
- package/dist/bin/ainecto.js +1781 -1569
- package/dist/bin/ainecto.js.map +1 -1
- package/dist/bin/mcp.js +180 -23
- package/dist/bin/mcp.js.map +1 -1
- package/package.json +1 -1
package/dist/bin/mcp.js
CHANGED
|
@@ -420,8 +420,8 @@ function toStoredToken(endpoint, token, metadata, previous) {
|
|
|
420
420
|
async function createLoopbackReceiver(expectedState, host = "127.0.0.1") {
|
|
421
421
|
let resolveCode;
|
|
422
422
|
let rejectCode;
|
|
423
|
-
const codePromise = new Promise((
|
|
424
|
-
resolveCode =
|
|
423
|
+
const codePromise = new Promise((resolve2, reject) => {
|
|
424
|
+
resolveCode = resolve2;
|
|
425
425
|
rejectCode = reject;
|
|
426
426
|
});
|
|
427
427
|
codePromise.catch(() => void 0);
|
|
@@ -452,12 +452,12 @@ async function createLoopbackReceiver(expectedState, host = "127.0.0.1") {
|
|
|
452
452
|
res.end("AiNecto CLI authorization complete. You can close this tab.");
|
|
453
453
|
resolveCode(code);
|
|
454
454
|
});
|
|
455
|
-
await new Promise((
|
|
455
|
+
await new Promise((resolve2, reject) => {
|
|
456
456
|
server.once("error", (error) => reject(new OAuthLoginError(
|
|
457
457
|
"CALLBACK_UNAVAILABLE",
|
|
458
458
|
`Could not start the local sign-in callback on ${host} (${error.message}). If this command runs in a sandbox, allow local network binding or run it outside the sandbox.`
|
|
459
459
|
)));
|
|
460
|
-
server.listen(0, host, () =>
|
|
460
|
+
server.listen(0, host, () => resolve2());
|
|
461
461
|
});
|
|
462
462
|
const address = server.address();
|
|
463
463
|
if (!address || typeof address === "string") {
|
|
@@ -466,9 +466,9 @@ async function createLoopbackReceiver(expectedState, host = "127.0.0.1") {
|
|
|
466
466
|
return {
|
|
467
467
|
redirectUri: `http://127.0.0.1:${address.port}/callback`,
|
|
468
468
|
waitForCode: () => codePromise,
|
|
469
|
-
close: () => new Promise((
|
|
469
|
+
close: () => new Promise((resolve2, reject) => {
|
|
470
470
|
server.closeAllConnections?.();
|
|
471
|
-
server.close((error) => error ? reject(error) :
|
|
471
|
+
server.close((error) => error ? reject(error) : resolve2());
|
|
472
472
|
})
|
|
473
473
|
};
|
|
474
474
|
}
|
|
@@ -497,7 +497,7 @@ function launchBrowser(url, deps = {}) {
|
|
|
497
497
|
const platform = deps.platform ?? process.platform;
|
|
498
498
|
const { command, args } = getBrowserOpenCommand(url, platform);
|
|
499
499
|
const spawnImpl = deps.spawnImpl ?? spawn;
|
|
500
|
-
return new Promise((
|
|
500
|
+
return new Promise((resolve2, reject) => {
|
|
501
501
|
const unavailable = (reason) => new OAuthLoginError(
|
|
502
502
|
"BROWSER_UNAVAILABLE",
|
|
503
503
|
`Could not open a browser on this machine (${command}: ${reason}). Sign-in needs a browser here.`
|
|
@@ -511,11 +511,11 @@ function launchBrowser(url, deps = {}) {
|
|
|
511
511
|
}
|
|
512
512
|
child.once("error", (error) => reject(unavailable(error.message)));
|
|
513
513
|
if (platform === "win32") {
|
|
514
|
-
child.once("spawn", () =>
|
|
514
|
+
child.once("spawn", () => resolve2());
|
|
515
515
|
} else {
|
|
516
516
|
child.once("exit", (code, signal) => {
|
|
517
517
|
if (code === 0) {
|
|
518
|
-
|
|
518
|
+
resolve2();
|
|
519
519
|
} else {
|
|
520
520
|
reject(unavailable(code === null ? `stopped by ${signal ?? "a signal"}` : `exited with code ${code}`));
|
|
521
521
|
}
|
|
@@ -547,13 +547,14 @@ function legacyRoleInput(flag, envVars = process.env) {
|
|
|
547
547
|
}
|
|
548
548
|
|
|
549
549
|
// src/core/version.ts
|
|
550
|
-
var CLI_VERSION = true ? "0.
|
|
550
|
+
var CLI_VERSION = true ? "0.10.0" : UNBUNDLED_VERSION;
|
|
551
551
|
var PACKAGE_NAME = "@ainecto/mcp";
|
|
552
552
|
var CLI_USER_AGENT = `${PACKAGE_NAME} ${CLI_VERSION}`;
|
|
553
553
|
|
|
554
554
|
// src/core/mcp/rpcClient.ts
|
|
555
555
|
var CHANNEL_HEADER = "X-AiNecto-Channel";
|
|
556
556
|
var AGENT_HEADER = "X-AiNecto-Agent";
|
|
557
|
+
var AGENT_NAME_HEADER = "X-AiNecto-Agent-Name";
|
|
557
558
|
var MAX_AGENT_NAME_LENGTH = 100;
|
|
558
559
|
var McpRpcError = class extends Error {
|
|
559
560
|
constructor(message, code, details) {
|
|
@@ -626,10 +627,11 @@ var McpRpcClient = class {
|
|
|
626
627
|
}
|
|
627
628
|
const text = await response.text();
|
|
628
629
|
if (!response.ok) {
|
|
630
|
+
const serverError = parseServerError(text);
|
|
629
631
|
throw new McpRpcError(
|
|
630
|
-
`MCP HTTP request failed with HTTP ${response.status}
|
|
632
|
+
`MCP HTTP request failed with HTTP ${response.status}${serverError ? ` \u2014 ${describeServerError(serverError)}` : "."}`,
|
|
631
633
|
"MCP_HTTP_ERROR",
|
|
632
|
-
httpErrorDetails(response.status, text, this.options.envVars ?? process.env)
|
|
634
|
+
{ ...httpErrorDetails(response.status, text, this.options.envVars ?? process.env), ...serverError ? { error: serverError.error } : {} }
|
|
633
635
|
);
|
|
634
636
|
}
|
|
635
637
|
if (!text.trim()) {
|
|
@@ -657,6 +659,9 @@ var McpRpcClient = class {
|
|
|
657
659
|
if (this.agent) {
|
|
658
660
|
headers[AGENT_HEADER.toLowerCase()] = this.agent;
|
|
659
661
|
}
|
|
662
|
+
if (this.options.agentName) {
|
|
663
|
+
headers[AGENT_NAME_HEADER.toLowerCase()] = this.options.agentName;
|
|
664
|
+
}
|
|
660
665
|
return this.fetchImpl(this.options.endpoint, {
|
|
661
666
|
method: "POST",
|
|
662
667
|
headers,
|
|
@@ -674,6 +679,25 @@ function printableHeaderValue(value) {
|
|
|
674
679
|
const printable = value.replace(/[^\x20-\x7E]/g, "").trim().slice(0, MAX_AGENT_NAME_LENGTH).trim();
|
|
675
680
|
return printable || void 0;
|
|
676
681
|
}
|
|
682
|
+
function parseServerError(body) {
|
|
683
|
+
let parsed;
|
|
684
|
+
try {
|
|
685
|
+
parsed = JSON.parse(body);
|
|
686
|
+
} catch {
|
|
687
|
+
return void 0;
|
|
688
|
+
}
|
|
689
|
+
if (!isRecord2(parsed) || typeof parsed.error !== "string" || !parsed.error.trim()) {
|
|
690
|
+
return void 0;
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
error: parsed.error.trim(),
|
|
694
|
+
description: typeof parsed.error_description === "string" && parsed.error_description.trim() ? parsed.error_description : void 0
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function describeServerError(serverError) {
|
|
698
|
+
const line = serverError.description ? `${serverError.error}: ${serverError.description}` : serverError.error;
|
|
699
|
+
return line.replace(/\s+/g, " ").trim();
|
|
700
|
+
}
|
|
677
701
|
function isJsonRpcResponse(value) {
|
|
678
702
|
return isRecord2(value) && value.jsonrpc === "2.0" && ("result" in value || "error" in value);
|
|
679
703
|
}
|
|
@@ -699,27 +723,49 @@ async function runStdioBridge(options) {
|
|
|
699
723
|
if (!line.trim()) {
|
|
700
724
|
continue;
|
|
701
725
|
}
|
|
726
|
+
let request;
|
|
727
|
+
try {
|
|
728
|
+
request = parseJsonRpcRequest(line);
|
|
729
|
+
} catch (error) {
|
|
730
|
+
writeLine(options.output, failure(null, error instanceof SyntaxError ? PARSE_ERROR : INVALID_REQUEST, error));
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
702
733
|
try {
|
|
703
|
-
const request = parseJsonRpcRequest(line);
|
|
704
734
|
if (request.method === "initialize") {
|
|
705
735
|
options.client.useAgent(clientInfoName(request.params));
|
|
706
736
|
}
|
|
707
737
|
const response = await options.client.sendRaw(request);
|
|
708
738
|
if (response && request.id !== void 0) {
|
|
709
|
-
options.output
|
|
710
|
-
`);
|
|
739
|
+
writeLine(options.output, response);
|
|
711
740
|
}
|
|
712
741
|
} catch (error) {
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
jsonrpc: "2.0",
|
|
716
|
-
id: null,
|
|
717
|
-
error: { code: -32603, message }
|
|
718
|
-
})}
|
|
742
|
+
if (request.id === void 0) {
|
|
743
|
+
options.errorOutput.write(`${errorMessage(error)}
|
|
719
744
|
`);
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
writeLine(options.output, failure(request.id, INTERNAL_ERROR, error));
|
|
720
748
|
}
|
|
721
749
|
}
|
|
722
750
|
}
|
|
751
|
+
var PARSE_ERROR = -32700;
|
|
752
|
+
var INVALID_REQUEST = -32600;
|
|
753
|
+
var INTERNAL_ERROR = -32603;
|
|
754
|
+
function failure(id, code, error) {
|
|
755
|
+
const data = error instanceof McpRpcError ? { code: error.code, ...isRecord3(error.details) ? error.details : {} } : void 0;
|
|
756
|
+
return {
|
|
757
|
+
jsonrpc: "2.0",
|
|
758
|
+
id,
|
|
759
|
+
error: { code, message: errorMessage(error), ...data ? { data } : {} }
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function errorMessage(error) {
|
|
763
|
+
return error instanceof Error ? error.message : String(error);
|
|
764
|
+
}
|
|
765
|
+
function writeLine(output, message) {
|
|
766
|
+
output.write(`${JSON.stringify(message)}
|
|
767
|
+
`);
|
|
768
|
+
}
|
|
723
769
|
function parseJsonRpcRequest(line) {
|
|
724
770
|
const parsed = JSON.parse(line);
|
|
725
771
|
if (!isRecord3(parsed) || parsed.jsonrpc !== "2.0" || typeof parsed.method !== "string") {
|
|
@@ -739,6 +785,109 @@ function isRecord3(value) {
|
|
|
739
785
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
740
786
|
}
|
|
741
787
|
|
|
788
|
+
// src/core/harness/agentTargets.ts
|
|
789
|
+
import { basename } from "path";
|
|
790
|
+
var SERVER_NAME = "ainecto";
|
|
791
|
+
var REMOTE_SIGN_IN = {
|
|
792
|
+
"claude-code": `Claude Code \u2014 in a session, run /mcp, pick "${SERVER_NAME}" and sign in`,
|
|
793
|
+
cursor: `Cursor \u2014 sign in when "${SERVER_NAME}" asks for it in Cursor's MCP settings`,
|
|
794
|
+
codex: `Codex \u2014 signs in while setup connects it (its sign-in link is printed above); if that did not finish, run \`codex mcp login ${SERVER_NAME}\``
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
// src/core/harness/harnessDoc.ts
|
|
798
|
+
var MARKER_BEGIN = "<!-- ainecto:begin -->";
|
|
799
|
+
var MARKER_END = "<!-- ainecto:end -->";
|
|
800
|
+
var HARNESS_DIR = ".ainecto";
|
|
801
|
+
var HARNESS_DOC_PATH = `${HARNESS_DIR}/HARNESS.md`;
|
|
802
|
+
var HARNESS_CONFIG_PATH = `${HARNESS_DIR}/config.json`;
|
|
803
|
+
var HARNESS_RECORD_PATH = `${HARNESS_DIR}/init-record.json`;
|
|
804
|
+
var DOC_KEY = "HARNESS.DOC";
|
|
805
|
+
var AGENT_NOTE_KEY = "HARNESS.AGENT_NOTE";
|
|
806
|
+
var REQUIRED_VARIABLES = {
|
|
807
|
+
[DOC_KEY]: ["endpoint"],
|
|
808
|
+
[AGENT_NOTE_KEY]: ["harnessDocPath", "serverName"]
|
|
809
|
+
};
|
|
810
|
+
var DEFAULT_AGENT_NOTE = `${MARKER_BEGIN}
|
|
811
|
+
## AiNecto Harness
|
|
812
|
+
|
|
813
|
+
This repository uses AiNecto as its design source of truth. Read \`{{harnessDocPath}}\` before doing
|
|
814
|
+
work that touches requirements, the ERD, or architecture.
|
|
815
|
+
|
|
816
|
+
AiNecto tools come from the \`{{serverName}}\` MCP server. Every session sees the same tools; the
|
|
817
|
+
signed-in user's AiNecto permissions decide what they may change, and the server refuses the rest.
|
|
818
|
+
${MARKER_END}`;
|
|
819
|
+
|
|
820
|
+
// src/core/harness/repositoryFiles.ts
|
|
821
|
+
import { lstat, readFile as readFile2, realpath } from "fs/promises";
|
|
822
|
+
import { dirname as dirname2, join as join2, resolve, sep } from "path";
|
|
823
|
+
async function findGitRoot(cwd) {
|
|
824
|
+
for (let at = resolve(cwd); ; at = dirname2(at)) {
|
|
825
|
+
try {
|
|
826
|
+
await lstat(join2(at, ".git"));
|
|
827
|
+
return at;
|
|
828
|
+
} catch {
|
|
829
|
+
}
|
|
830
|
+
if (dirname2(at) === at) {
|
|
831
|
+
return void 0;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
async function readManagedFile(root, path) {
|
|
836
|
+
await assertInsideRepository(root, path);
|
|
837
|
+
return readIfExists(join2(root, path));
|
|
838
|
+
}
|
|
839
|
+
async function readIfExists(absolutePath) {
|
|
840
|
+
try {
|
|
841
|
+
return await readFile2(absolutePath, "utf8");
|
|
842
|
+
} catch (error) {
|
|
843
|
+
if (isNodeError2(error, "ENOENT")) {
|
|
844
|
+
return void 0;
|
|
845
|
+
}
|
|
846
|
+
throw error;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
async function assertInsideRepository(root, relativePath) {
|
|
850
|
+
const absolutePath = join2(root, relativePath);
|
|
851
|
+
const realRoot = await realpath(root);
|
|
852
|
+
let realParent;
|
|
853
|
+
try {
|
|
854
|
+
realParent = await realpath(dirname2(absolutePath));
|
|
855
|
+
} catch (error) {
|
|
856
|
+
if (isNodeError2(error, "ENOENT")) {
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
throw error;
|
|
860
|
+
}
|
|
861
|
+
if (realParent !== realRoot && !realParent.startsWith(realRoot + sep)) {
|
|
862
|
+
throw new Error(
|
|
863
|
+
`${relativePath} resolves outside the repository (${realParent}); refusing to touch it.`
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
try {
|
|
867
|
+
if ((await lstat(absolutePath)).isSymbolicLink()) {
|
|
868
|
+
throw new Error(`${relativePath} is a symbolic link; refusing to write through it.`);
|
|
869
|
+
}
|
|
870
|
+
} catch (error) {
|
|
871
|
+
if (!isNodeError2(error, "ENOENT")) {
|
|
872
|
+
throw error;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
function isNodeError2(error, code) {
|
|
877
|
+
return typeof error === "object" && error !== null && error.code === code;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// src/core/harness/agentName.ts
|
|
881
|
+
var AGENT_NAME_PATH = `${HARNESS_DIR}/agent.local`;
|
|
882
|
+
function agentNameOf(raw) {
|
|
883
|
+
const name = raw?.trim();
|
|
884
|
+
return name ? name : void 0;
|
|
885
|
+
}
|
|
886
|
+
async function readAgentName(cwd) {
|
|
887
|
+
const root = await findGitRoot(cwd) ?? cwd;
|
|
888
|
+
return agentNameOf(await readManagedFile(root, AGENT_NAME_PATH));
|
|
889
|
+
}
|
|
890
|
+
|
|
742
891
|
// src/adapters/mcp/connector.ts
|
|
743
892
|
async function runConnector(options) {
|
|
744
893
|
const legacyRole = legacyRoleInput(options.legacyRoleFlag, options.envVars);
|
|
@@ -751,7 +900,8 @@ async function runConnector(options) {
|
|
|
751
900
|
endpoint: options.endpoint,
|
|
752
901
|
tokenProvider: auth,
|
|
753
902
|
legacyRole,
|
|
754
|
-
|
|
903
|
+
agentName: options.agentFlag ?? await readAgentName(options.cwd ?? process.cwd()),
|
|
904
|
+
// 에이전트의 MCP 연결 — 에이전트 «종류»는 브리지가 호스트의 initialize 에서 읽어 싣는다(«이름»은 위 agentName).
|
|
755
905
|
channel: "mcp"
|
|
756
906
|
});
|
|
757
907
|
await runStdioBridge({
|
|
@@ -829,6 +979,12 @@ function parseMcpArgs(argv) {
|
|
|
829
979
|
result.env = assertEnv(requireFlagValue(arg, argv[++index]));
|
|
830
980
|
} else if (arg === "--endpoint") {
|
|
831
981
|
result.endpoint = requireFlagValue(arg, argv[++index]);
|
|
982
|
+
} else if (arg === "--agent") {
|
|
983
|
+
const name = agentNameOf(requireFlagValue(arg, argv[++index]));
|
|
984
|
+
if (name === void 0) {
|
|
985
|
+
throw new Error(`${arg} requires a value.`);
|
|
986
|
+
}
|
|
987
|
+
result.agent = name;
|
|
832
988
|
} else if (arg === "--role") {
|
|
833
989
|
const value = argv[index + 1];
|
|
834
990
|
if (value !== void 0 && !value.startsWith("--")) {
|
|
@@ -854,13 +1010,14 @@ function requireFlagValue(flag, value) {
|
|
|
854
1010
|
try {
|
|
855
1011
|
const args = parseMcpArgs(process.argv.slice(2));
|
|
856
1012
|
if (args.help) {
|
|
857
|
-
process.stdout.write("Usage: mcp [--env prod|dev] [--endpoint URL]\n");
|
|
1013
|
+
process.stdout.write("Usage: mcp [--env prod|dev] [--endpoint URL] [--agent NAME]\n");
|
|
858
1014
|
process.exit(0);
|
|
859
1015
|
}
|
|
860
1016
|
const resolved = resolveEndpoint({ env: args.env, endpoint: args.endpoint });
|
|
861
1017
|
await runConnector({
|
|
862
1018
|
endpoint: resolved.endpoint,
|
|
863
1019
|
legacyRoleFlag: args.legacyRole,
|
|
1020
|
+
agentFlag: args.agent,
|
|
864
1021
|
input: process.stdin,
|
|
865
1022
|
output: process.stdout,
|
|
866
1023
|
errorOutput: process.stderr
|