agentlas 0.9.2 → 0.9.4
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/CHANGELOG.md +19 -0
- package/engine/agentlas-repl.cjs +1 -0
- package/engine/agentlas.cjs +127 -31
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.4 — 2026-07-23
|
|
4
|
+
|
|
5
|
+
- `plugin add` no longer registers a code-hosting page (GitHub/GitLab/Bitbucket
|
|
6
|
+
repo or homepage URL) as if it were a live MCP server, even when a manifest
|
|
7
|
+
row explicitly claims `transport:"http"`. A connectorless catalog entry now
|
|
8
|
+
refuses honestly with its docs link instead of writing an unreachable
|
|
9
|
+
server into the local MCP config.
|
|
10
|
+
- stdio rows (`command`+`args`+`envKeys`) from a plugin manifest now install
|
|
11
|
+
correctly into the local MCP server registry.
|
|
12
|
+
- `plugin-add-contract` runs as part of the regular smoke suite.
|
|
13
|
+
|
|
14
|
+
## 0.9.3 — 2026-07-20
|
|
15
|
+
|
|
16
|
+
- Preserve the terminal UI spinner lifecycle through the memory-output guard,
|
|
17
|
+
so completed one-shot Claude/Codex turns exit cleanly instead of throwing
|
|
18
|
+
after the model result has already been printed.
|
|
19
|
+
- Keep Agentlas Terminal focused on independent agent and team execution; this
|
|
20
|
+
release does not add an Agentlas One surface.
|
|
21
|
+
|
|
3
22
|
## 0.9.2 — 2026-07-20
|
|
4
23
|
|
|
5
24
|
- Require the installed or Desktop-bundled Agentlas Core runtime to include the
|
package/engine/agentlas-repl.cjs
CHANGED
package/engine/agentlas.cjs
CHANGED
|
@@ -2210,43 +2210,107 @@ async function fetchPluginManifestCli(slug) {
|
|
|
2210
2210
|
return manifest;
|
|
2211
2211
|
}
|
|
2212
2212
|
|
|
2213
|
-
|
|
2213
|
+
// 레포/홈페이지 HTML 페이지는 문서지 MCP 연결이 아니다. 이 URL들을 transport:"http"로
|
|
2214
|
+
// 등록하면 "절대 연결될 수 없는 MCP 서버"가 생긴다(2026-07-23 근본수리 계열).
|
|
2215
|
+
const PLUGIN_CODE_HOSTING_HTML_RE = /^https?:\/\/(www\.)?(github\.com|gitlab\.com|bitbucket\.org)\//i;
|
|
2216
|
+
|
|
2217
|
+
/** 휴리스틱: 명시적 transport 선언이 없는 레거시 source URL이 진짜 MCP 엔드포인트로 보이는가. */
|
|
2218
|
+
function pluginLooksLikeMcpEndpointCli(rawUrl) {
|
|
2219
|
+
let parsed;
|
|
2220
|
+
try {
|
|
2221
|
+
parsed = new URL(String(rawUrl || ""));
|
|
2222
|
+
} catch {
|
|
2223
|
+
return false;
|
|
2224
|
+
}
|
|
2225
|
+
if (!/^https?:$/.test(parsed.protocol)) return false;
|
|
2226
|
+
if (PLUGIN_CODE_HOSTING_HTML_RE.test(parsed.href)) return false;
|
|
2227
|
+
const pathname = parsed.pathname.replace(/\/+$/, "");
|
|
2228
|
+
if (/\/(mcp|sse)$/i.test(pathname)) return true; // …/mcp, …/sse 관례
|
|
2229
|
+
if (/^mcp\./i.test(parsed.hostname)) return true; // mcp.linear.app 류 전용 호스트
|
|
2230
|
+
return false;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
/**
|
|
2234
|
+
* 매니페스트의 mcp[] 항목을 mcp_servers 행으로 정규화. stdio(command)와 remote(url)를 구분한다.
|
|
2235
|
+
* 반환: { row } | { refused: { name, source, reason } } | null(빈 항목).
|
|
2236
|
+
*
|
|
2237
|
+
* 규칙(근본수리): 레포 URL은 어떤 경우에도 transport:"http" 행으로 쓰이지 않는다.
|
|
2238
|
+
* - transport:"stdio" + command 명시 → stdio 행 (mcp_servers는 command/args_json을 이미 지원).
|
|
2239
|
+
* - transport:"http" + url 명시 → http 행. 단 코드호스팅 HTML 페이지(github.com/…)면 거부.
|
|
2240
|
+
* - 레거시 {name, source}: http(s)면 MCP 엔드포인트로 보일 때만(…/mcp, …/sse, mcp.* 호스트) 수용,
|
|
2241
|
+
* 아니면 거부. 비-URL 문자열은 기존대로 stdio 실행 커맨드로 해석.
|
|
2242
|
+
*/
|
|
2214
2243
|
function pluginMcpRowCli(slug, entry, index) {
|
|
2215
|
-
const source = typeof entry?.source === "string" ? entry.source.trim() : "";
|
|
2216
2244
|
const name = (typeof entry?.name === "string" && entry.name.trim()) || `${slug}-${index + 1}`;
|
|
2217
|
-
const
|
|
2245
|
+
const source = typeof entry?.source === "string" ? entry.source.trim() : "";
|
|
2246
|
+
const transport = typeof entry?.transport === "string" ? entry.transport.trim().toLowerCase() : "";
|
|
2247
|
+
const envKeys = Array.isArray(entry?.envKeys)
|
|
2248
|
+
? entry.envKeys.filter((key) => typeof key === "string")
|
|
2249
|
+
: entry?.env && typeof entry.env === "object"
|
|
2250
|
+
? Object.keys(entry.env)
|
|
2251
|
+
: [];
|
|
2252
|
+
const makeRow = (fields) => ({
|
|
2253
|
+
row: {
|
|
2254
|
+
id: require("node:crypto").randomUUID(),
|
|
2255
|
+
catalogId: `hub:${slug}:${name}`,
|
|
2256
|
+
name,
|
|
2257
|
+
envKeysJson: JSON.stringify(envKeys),
|
|
2258
|
+
...fields,
|
|
2259
|
+
},
|
|
2260
|
+
});
|
|
2261
|
+
const refuse = (reason) => ({ refused: { name, source: source || (typeof entry?.url === "string" ? entry.url : ""), reason } });
|
|
2262
|
+
|
|
2263
|
+
if (transport === "stdio") {
|
|
2264
|
+
const command = typeof entry?.command === "string" ? entry.command.trim() : "";
|
|
2265
|
+
if (!command) return refuse("stdio row without a launch command");
|
|
2266
|
+
const args = Array.isArray(entry?.args) ? entry.args.filter((a) => typeof a === "string") : [];
|
|
2267
|
+
return makeRow({ transport: "stdio", command, argsJson: JSON.stringify(args), url: null });
|
|
2268
|
+
}
|
|
2269
|
+
if (transport === "http" || transport === "sse") {
|
|
2270
|
+
const url = typeof entry?.url === "string" && entry.url.trim() ? entry.url.trim() : source;
|
|
2271
|
+
if (!/^https?:\/\//i.test(url)) return refuse("http row without a usable endpoint URL");
|
|
2272
|
+
if (PLUGIN_CODE_HOSTING_HTML_RE.test(url)) {
|
|
2273
|
+
return refuse("URL is a code-hosting HTML page (docs), not an MCP endpoint");
|
|
2274
|
+
}
|
|
2275
|
+
// 명시적 transport 선언은 서버가 검증한 연결정보로 신뢰한다 (레포 페이지만 방어).
|
|
2276
|
+
return makeRow({ transport: "http", command: null, argsJson: "[]", url });
|
|
2277
|
+
}
|
|
2278
|
+
if (transport) return refuse(`unsupported transport "${transport}"`);
|
|
2279
|
+
|
|
2280
|
+
// ── 레거시 {name, source} 행 ──
|
|
2281
|
+
if (!source) return null;
|
|
2282
|
+
if (/^https?:\/\//i.test(source)) {
|
|
2283
|
+
if (!pluginLooksLikeMcpEndpointCli(source)) {
|
|
2284
|
+
return refuse(
|
|
2285
|
+
PLUGIN_CODE_HOSTING_HTML_RE.test(source)
|
|
2286
|
+
? "URL is a code-hosting HTML page (docs), not an MCP endpoint"
|
|
2287
|
+
: "URL does not look like an MCP endpoint (no /mcp, /sse, or mcp.* host, and no declared transport)",
|
|
2288
|
+
);
|
|
2289
|
+
}
|
|
2290
|
+
return makeRow({ transport: "http", command: null, argsJson: "[]", url: source });
|
|
2291
|
+
}
|
|
2218
2292
|
// 원격은 URL, stdio는 실행 커맨드다. 둘을 섞으면 codex config.toml 스키마 위반으로
|
|
2219
2293
|
// 런타임이 통째로 죽는다(Runtime Doctor가 반복해서 잡던 사고 계열).
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
return {
|
|
2223
|
-
id: require("node:crypto").randomUUID(),
|
|
2224
|
-
catalogId: `hub:${slug}:${name}`,
|
|
2225
|
-
name,
|
|
2226
|
-
transport: remote ? "http" : "stdio",
|
|
2227
|
-
command: remote ? null : (argv[0] ?? null),
|
|
2228
|
-
argsJson: JSON.stringify(remote ? [] : argv.slice(1)),
|
|
2229
|
-
url: remote ? source : null,
|
|
2230
|
-
envKeysJson: JSON.stringify(
|
|
2231
|
-
Array.isArray(entry?.envKeys) ? entry.envKeys.filter((key) => typeof key === "string") : [],
|
|
2232
|
-
),
|
|
2233
|
-
};
|
|
2294
|
+
const argv = source.split(/\s+/).filter(Boolean);
|
|
2295
|
+
return makeRow({ transport: "stdio", command: argv[0] ?? null, argsJson: JSON.stringify(argv.slice(1)), url: null });
|
|
2234
2296
|
}
|
|
2235
2297
|
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
const
|
|
2239
|
-
|
|
2240
|
-
const
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2298
|
+
/** 매니페스트 전체를 설치 계획으로 정규화: 등록할 행과, 정직하게 거부한 항목을 분리한다. */
|
|
2299
|
+
function planPluginMcpInstallCli(slug, manifest) {
|
|
2300
|
+
const entries = Array.isArray(manifest?.mcp) ? manifest.mcp : [];
|
|
2301
|
+
const rows = [];
|
|
2302
|
+
const refused = [];
|
|
2303
|
+
entries.forEach((entry, index) => {
|
|
2304
|
+
const normalized = pluginMcpRowCli(slug, entry, index);
|
|
2305
|
+
if (!normalized) return;
|
|
2306
|
+
if (normalized.row) rows.push(normalized.row);
|
|
2307
|
+
else if (normalized.refused) refused.push(normalized.refused);
|
|
2308
|
+
});
|
|
2309
|
+
return { rows, refused };
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
/** 정규화된 행들을 로컬 mcp_servers 스키마에 멱등 삽입. { installed, reused } 반환. */
|
|
2313
|
+
function installPluginMcpRowsCli(db, rows) {
|
|
2250
2314
|
let installed = 0;
|
|
2251
2315
|
let reused = 0;
|
|
2252
2316
|
for (const row of rows) {
|
|
@@ -2261,7 +2325,34 @@ async function cmdPluginAdd(db, slug) {
|
|
|
2261
2325
|
);
|
|
2262
2326
|
installed += 1;
|
|
2263
2327
|
}
|
|
2328
|
+
return { installed, reused };
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
async function cmdPluginAdd(db, slug) {
|
|
2332
|
+
if (!slug) fail('usage: agentlas plugin add <slug> (run agentlas plugin list first)');
|
|
2333
|
+
const manifest = await fetchPluginManifestCli(slug);
|
|
2334
|
+
if (!manifest) fail(`Hub plugin not found: ${slug}`);
|
|
2335
|
+
const { rows, refused } = planPluginMcpInstallCli(slug, manifest);
|
|
2336
|
+
const docsLink = manifest.docs || manifest.source?.repo || manifest.source?.homepage || null;
|
|
2337
|
+
if (!rows.length) {
|
|
2338
|
+
// 설치할 MCP 서버가 없으면 조용히 성공했다고 하지 않는다 — 사용자는 이 플러그인이
|
|
2339
|
+
// 붙었다고 믿고 도구를 기대하게 된다. 레포 URL을 http MCP 서버로 등록하는 일도
|
|
2340
|
+
// 절대 하지 않는다(연결 불가능한 가짜 서버).
|
|
2341
|
+
const reasonLines = refused.map((item) => ` ✗ ${item.name}: ${item.reason}${item.source ? ` (${item.source})` : ""}`);
|
|
2342
|
+
fail(
|
|
2343
|
+
[
|
|
2344
|
+
`${slug} ships no machine-connectable MCP endpoint yet. Nothing was registered.`,
|
|
2345
|
+
...reasonLines,
|
|
2346
|
+
docsLink ? ` docs: ${docsLink} (upstream project page — not an MCP endpoint)` : null,
|
|
2347
|
+
" When the catalog gains verified connection info for this plugin, re-run: agentlas plugin add " + slug,
|
|
2348
|
+
].filter(Boolean).join("\n"),
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
2351
|
+
const { installed, reused } = installPluginMcpRowsCli(db, rows);
|
|
2264
2352
|
out(`✓ Plugin installed ${manifest.slug} — ${manifest.name}`);
|
|
2353
|
+
for (const item of refused) {
|
|
2354
|
+
out(` ⚠ skipped ${item.name}: ${item.reason}${item.source ? ` (${item.source})` : ""}`);
|
|
2355
|
+
}
|
|
2265
2356
|
out(` MCP servers: ${installed} added${reused ? `, ${reused} already present` : ""}`);
|
|
2266
2357
|
const authKind = manifest.auth?.kind;
|
|
2267
2358
|
if (authKind && authKind !== "none") {
|
|
@@ -11875,4 +11966,9 @@ module.exports = {
|
|
|
11875
11966
|
autoRouteNote,
|
|
11876
11967
|
autoRoutePreamble,
|
|
11877
11968
|
directSystemPrompt,
|
|
11969
|
+
// Hub 플러그인 설치 회귀 테스트 표면 — 레포 URL을 MCP 서버로 등록하지 않는 규칙 검증용.
|
|
11970
|
+
pluginMcpRowCli,
|
|
11971
|
+
planPluginMcpInstallCli,
|
|
11972
|
+
installPluginMcpRowsCli,
|
|
11973
|
+
pluginLooksLikeMcpEndpointCli,
|
|
11878
11974
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlas",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"description": "Agentlas agent terminal — chat with your installed AI agents and teams from the terminal, Claude Code style. Standalone: no desktop app required.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agentlas": "bin/agentlas.cjs"
|