agentlas 1.0.14 → 1.0.16
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 +24 -0
- package/engine/agentlas-cloud-runtime.cjs +8 -2
- package/engine/agentlas-workforce.cjs +34 -17
- package/engine/cloud-assets/cas.cjs +7 -2
- package/engine/cloud-assets/commands.cjs +173 -2
- package/engine/cloud-assets/package.cjs +77 -1
- package/engine/cloud-assets/state.cjs +35 -0
- package/engine/commands/plugin.cjs +82 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.16 — 2026-07-30
|
|
4
|
+
|
|
5
|
+
- **Project-scoped plugin listing now fails closed.** A missing, unsafe, or
|
|
6
|
+
uninitialized `--project` path returns a clear error instead of silently
|
|
7
|
+
showing the global Hub catalog as if project compatibility had been checked.
|
|
8
|
+
- Plugin listing names its actual scope: the global Hub catalog does not claim
|
|
9
|
+
to evaluate project compatibility or local installation state.
|
|
10
|
+
- Unknown, conflicting, or incomplete plugin flags now return usage errors
|
|
11
|
+
instead of being ignored.
|
|
12
|
+
|
|
13
|
+
## 1.0.15 — 2026-07-29
|
|
14
|
+
|
|
15
|
+
- **Agent Cloud saves work from a new machine or fresh clone.** Before writing,
|
|
16
|
+
the terminal resolves the signed-in owner's exact asset revision by
|
|
17
|
+
`slug` and scope, then keeps the conditional-write guard on that revision.
|
|
18
|
+
A missing local receipt no longer turns an owned asset into a false create
|
|
19
|
+
conflict.
|
|
20
|
+
- **A real stale-copy conflict stays fail-closed.** The terminal stops before
|
|
21
|
+
replacing a newer server revision, reports its identity, and offers an
|
|
22
|
+
explicit `--overwrite` only when the owner deliberately chooses the current
|
|
23
|
+
folder over the newer copy.
|
|
24
|
+
- Conflict messages now describe ownership and the next command instead of
|
|
25
|
+
exposing storage precondition terminology.
|
|
26
|
+
|
|
3
27
|
## 1.0.14 — 2026-07-29
|
|
4
28
|
|
|
5
29
|
- **Write-capable commands now prepare every project automatically.** The first
|
|
@@ -74,7 +74,7 @@ function buildManifest(root, options = {}) {
|
|
|
74
74
|
memoryPolicy: { writeBack: "ask", publicCopy: "reset" },
|
|
75
75
|
memory: files.filter((file) => [".agentlas/memory-map.json", ".agentlas/agent-card.json"].includes(file.path)).map((file) => file.path),
|
|
76
76
|
allowRead: ["README.md", "AGENTS.md", "agent.md", "skills/**", ".agentlas/*.json"],
|
|
77
|
-
denyRead: [".env", ".env.*", "**/secrets/**", "
|
|
77
|
+
denyRead: [".env", ".env.*", "secrets/**", "**/secrets/**", "credentials/**", "**/credentials/**", "cookies/**", "**/cookies/**"],
|
|
78
78
|
publicExportPolicy: "clean-copy",
|
|
79
79
|
requiredRuntime: ["mcp-client"],
|
|
80
80
|
license: "call-only-default",
|
|
@@ -92,7 +92,13 @@ function scanFiles(files) {
|
|
|
92
92
|
findings.push({ verdict, type, path: file.path, ...(line ? { line } : {}), message, redacted: true });
|
|
93
93
|
}
|
|
94
94
|
for (const file of files) {
|
|
95
|
-
|
|
95
|
+
// A credential store is a path SEGMENT, never a filename substring. The old
|
|
96
|
+
// `/token|secret/i` test blocked ordinary vocabulary — measured 2026-07-29,
|
|
97
|
+
// it gave the live `web-master` package a BLOCK verdict for its own design
|
|
98
|
+
// system (`token-architecture.md`, `reference-token-db.json`). Dropping it
|
|
99
|
+
// costs no coverage: the per-line SECRET_PATTERNS scan below reads every
|
|
100
|
+
// packaged file and is the check that actually looks at values.
|
|
101
|
+
if (/^\.env(\.|$)/.test(file.path.split("/").pop() || "") || /(?:^|\/)(secrets|credentials|cookies)\//i.test(file.path)) {
|
|
96
102
|
add("BLOCK", "credential-path", file, null, "Credential-like file path is excluded from Cloud package and public publish.");
|
|
97
103
|
}
|
|
98
104
|
file.content.split(/\r?\n/).forEach((line, index) => {
|
|
@@ -142,13 +142,14 @@ const HUB_SECRET_PATTERNS = [
|
|
|
142
142
|
["credential_assignment", /\b(?:api[_-]?key|access[_-]?key|client[_-]?secret|secret|token|password|passwd|cookie)\s*[:=]\s*['"]?[^\s'";,]{8,}/i],
|
|
143
143
|
["credential_url", /\b[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\s/:@]+:[^\s/@]+@[^\s/]+/i],
|
|
144
144
|
];
|
|
145
|
-
const
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
"
|
|
149
|
-
"
|
|
150
|
-
"
|
|
151
|
-
"
|
|
145
|
+
const WORKFORCE_ONTOLOGY_GUIDE = [
|
|
146
|
+
"Roles, communities, skills and knowledge are open-world English semantic IDs. Seed values are aliases and graph anchors, never allowlists. Author a new faithful namespaced ID when no seed expresses the work.",
|
|
147
|
+
"Community seed examples: community:software-engineering, community:backend-engineering, community:frontend-engineering, community:database-engineering, community:payments-engineering, community:quality-engineering, community:security-engineering, community:data-engineering, community:ai-engineering, community:devops, community:product-design, community:research, community:marketing, community:finance, community:legal, community:travel, community:operations, community:agent-systems.",
|
|
148
|
+
"Role seed examples: role:software-architect, role:backend-engineer, role:frontend-engineer, role:database-engineer, role:payments-engineer, role:quality-engineer, role:security-engineer, role:ontology-architect, role:agent-runtime-engineer, role:researcher, role:travel-planner.",
|
|
149
|
+
"Skill seed examples: skill:software-architecture, skill:api-design, skill:server-implementation, skill:frontend-implementation, skill:data-modeling, skill:test-design, skill:verification, skill:ontology-modeling, skill:knowledge-graph-design, skill:evidence-synthesis, skill:travel-planning.",
|
|
150
|
+
"Tool capability seed examples: tool:file-system, tool:file-read, tool:file-write, tool:shell, tool:web-search, tool:browser, tool:mongodb, tool:database, tool:github, tool:payments. Tool binding remains a prepare-time runtime concern.",
|
|
151
|
+
"Use artifact:<kind> only for edges.artifactKinds. Do not fill consumes, produces, requiredRoles, requiredToolCapabilities, requiredAuthorities, forbiddenAuthorities, or modalities on a slot — tools, authorities, and modalities attach to the executing runtime, not the agent card, and card-declaration gates on those fields only exclude real candidates. Put ordinary workflow inputs, outputs, and handoffs in the slot task and edges.artifactKinds. Express desired role fit through title, task, optionalCommunities, and optionalSkills.",
|
|
152
|
+
"Use a broad required community for the job-family boundary, put desired expertise in optional communities/skills plus the role task, and let the host LLM judge title, summary and semantic evidence.",
|
|
152
153
|
"forbiddenCommunities and excludedCommunities are not exhaustive lists of every unused job family. Add only an explicit user prohibition or an inherent incompatibility with the assignment. Never forbid a broad ancestor, descendant, adjacent, or legitimately co-occurring community merely because another community was selected.",
|
|
153
154
|
].join("\n");
|
|
154
155
|
|
|
@@ -1013,6 +1014,23 @@ function validateWorkOrder(value) {
|
|
|
1013
1014
|
}
|
|
1014
1015
|
const slots = assertArray(order.roleSlots, "workOrder.roleSlots", MAX_SLOTS, { min: 1 });
|
|
1015
1016
|
const seen = new Set();
|
|
1017
|
+
// An absent list-valued slot field IS the empty constraint (2026-07-30):
|
|
1018
|
+
// normalize (absent -> []) BEFORE validation and before the order is
|
|
1019
|
+
// digested or sent, so a lean-form author and a full-form author produce
|
|
1020
|
+
// byte-identical canonical orders. Full forms pass through untouched.
|
|
1021
|
+
const SLOT_LIST_FIELDS = [
|
|
1022
|
+
"requiredCommunities", "optionalCommunities", "excludedCommunities",
|
|
1023
|
+
"requiredRoles", "requiredSkills", "optionalSkills", "requiredKnowledge",
|
|
1024
|
+
"requiredToolCapabilities", "consumes", "produces", "requiredAuthorities",
|
|
1025
|
+
"forbiddenAuthorities", "runtimes", "languages", "modalities",
|
|
1026
|
+
];
|
|
1027
|
+
for (let index = 0; index < slots.length; index += 1) {
|
|
1028
|
+
if (slots[index] && typeof slots[index] === "object" && !Array.isArray(slots[index])) {
|
|
1029
|
+
for (const field of SLOT_LIST_FIELDS) {
|
|
1030
|
+
if (!Object.prototype.hasOwnProperty.call(slots[index], field)) slots[index][field] = [];
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1016
1034
|
for (let index = 0; index < slots.length; index += 1) {
|
|
1017
1035
|
const slot = assertObject(slots[index], `roleSlots[${index}]`);
|
|
1018
1036
|
assertExactKeys(slot, [
|
|
@@ -1850,14 +1868,14 @@ function buildPrompts(task, identity) {
|
|
|
1850
1868
|
"Return the direct agentlas.workforce-work-order.v1 JSON object. Do not emit schemaVersion=agentlas.workforce-leader-call.v1 and do not emit toolCall, name, or arguments wrappers. The host invokes workforce.search_candidates with your exact validated WorkOrder.",
|
|
1851
1869
|
"The direct WorkOrder top level must contain exactly: schemaVersion, workOrderId, taskBrief, redacted, ontologyVersion, roleSlots, edges, forbiddenCommunities, selectionPolicy.",
|
|
1852
1870
|
`Exact direct WorkOrder example: ${stableJson(workOrderShape)}`,
|
|
1853
|
-
"Every roleSlots item must contain
|
|
1854
|
-
"consumes
|
|
1871
|
+
"Every roleSlots item must contain slotId, title, task, cardinality, criticality, and allowedEntityKinds; minimumEvidenceLevel is optional. Constrain the hire only through requiredCommunities, optionalCommunities, excludedCommunities, requiredSkills, optionalSkills, requiredKnowledge, runtimes, and languages, and only when the constraint is genuine. Any list field you leave out is the empty constraint — the host normalizes absent to [] before validation and digest, so omit empty ones instead of spelling [].",
|
|
1872
|
+
"Never fill requiredToolCapabilities, requiredAuthorities, forbiddenAuthorities, consumes, produces, requiredRoles, or modalities: tools, authorities, and modalities attach to the executing runtime, not the agent card, and those gates only exclude real candidates. Describe normal inputs/outputs in task and represent inter-slot handoffs with edges and edges.artifactKinds.",
|
|
1855
1873
|
"workOrderId and every concept/reference id must match [A-Za-z0-9][A-Za-z0-9._:/@-]{1,255} and have total length at most 255 characters. taskBrief is limited to 64000 characters; each slot title to 160 and slot task to 32000 — describe each responsibility as fully as the work honestly needs. Each id array is limited to 256 unique items.",
|
|
1856
1874
|
"roleSlots must contain 1-32 items. cardinality must be an integer from 1 through 16. criticality must be exactly required or optional. allowedEntityKinds must be a non-empty unique subset of executable agent, team. group is ontology/discovery metadata and cannot be executed. minimumEvidenceLevel, when authored, must be exactly declared, checked, demonstrated, or attested.",
|
|
1857
1875
|
"edges must contain at most 128 items. Every edge must contain exactly from, to, relation, and artifactKinds. from and to must reference declared slotId values. relation must be exactly one of reportsTo, handsOffTo, reviews, coordinatesWith.",
|
|
1858
1876
|
"forbiddenCommunities and edges must be explicitly authored arrays. selectionPolicy must contain exactly allowHistoryEvidence=false, integer minimumCandidatesPerSlot from 2 through 30, and integer maximumCandidatesPerSlot from 2 through 100 that is not below the minimum.",
|
|
1859
1877
|
"A community cannot appear in forbiddenCommunities or a slot's excludedCommunities when that same slot requires or optionally prefers it. Also avoid broader ancestor, descendant, adjacent, and legitimately co-occurring exclusions; the host rejects exact contradictions but does not invent ontology lineage or mutate your decision.",
|
|
1860
|
-
`redacted must be true and ontologyVersion must be exactly ${WORKFORCE_ONTOLOGY_VERSION}
|
|
1878
|
+
`redacted must be true and ontologyVersion must be exactly ${WORKFORCE_ONTOLOGY_VERSION}; this pins graph semantics, not a finite vocabulary. Write all discovery-facing content in English and use faithful open namespaced concept IDs.`,
|
|
1861
1879
|
].join("\n");
|
|
1862
1880
|
const selectionSchemaRequirements = [
|
|
1863
1881
|
"Return the direct agentlas.workforce-selection.v1 JSON object. Do not emit schemaVersion=agentlas.workforce-leader-call.v1 and do not emit toolCall, name, or arguments wrappers. The host invokes workforce.validate_selection with your exact validated Selection.",
|
|
@@ -1887,15 +1905,14 @@ function buildPrompts(task, identity) {
|
|
|
1887
1905
|
"Any specialized domain explicitly present in the task with distinct failure or accountability semantics must have its own accountable domain slot. Examples include payments, insurance, legal, finance, travel, and regulated science or operations. Never collapse such a named domain into generic backend, software, database, or implementation work. This is a general job-analysis rule, not a fixed list of required professions.",
|
|
1888
1906
|
"forbiddenCommunities is not the inverse of selected communities and not an exhaustive list of unused professions. Add a global or slot exclusion only when the user explicitly prohibited that community or when participation is inherently incompatible with the assignment. Empty exclusion arrays are correct when no such negative constraint exists.",
|
|
1889
1907
|
"Never forbid or exclude a broad ancestor, descendant, adjacent, or legitimately co-occurring community merely because a narrower job family was selected. Check every exclusion against all requiredCommunities and optionalCommunities before returning JSON.",
|
|
1890
|
-
"Hard requirements mean absence makes the assignment impossible and the Hub catalog must prove eligibility; importance alone is not a hard gate. Prefer a broad required community plus optional skills when legacy declarations may be sparse.
|
|
1891
|
-
"
|
|
1892
|
-
"consumes and produces require the selected Hub candidate profile itself to declare those exact artifacts. Ordinary workflow dependencies and handoffs belong in task and edges, not these hard fields.",
|
|
1908
|
+
"Hard requirements mean absence makes the assignment impossible and the Hub catalog must prove eligibility; importance alone is not a hard gate. Prefer a broad required community plus optional skills when legacy declarations may be sparse. Do not author requiredRoles; express desired role fit through title, task, optionalCommunities, and optionalSkills.",
|
|
1909
|
+
"Do not author requiredToolCapabilities, requiredAuthorities, forbiddenAuthorities, consumes, produces, or modalities: tools, authorities, and modalities attach to the executing runtime, not the agent card, and card-declaration gates on those fields only exclude real candidates. Ordinary workflow dependencies and handoffs belong in task and edges.",
|
|
1893
1910
|
"Before returning JSON, self-check that every explicitly named specialized domain responsibility is independently represented, every primary domain responsibility has an accountable slot, every exclusion is explicit or inherently incompatible and does not conflict with job-family lineage, requiredRoles is empty unless strictly indispensable, and every other hard field passes the execution-impossible test.",
|
|
1894
1911
|
"Return exactly one direct WorkOrder JSON object. Do not choose agents yet. Do not use ratings, popularity, invocation history, or revenue.",
|
|
1895
|
-
"
|
|
1912
|
+
"Explicitly author every structural field (slotId, title, task, cardinality, criticality, allowedEntityKinds, edges, forbiddenCommunities, selectionPolicy). A list-valued slot field you leave out is the empty constraint; the host normalizes absent to [].",
|
|
1896
1913
|
"Never copy secrets, local file contents, account identifiers, or private memory into taskBrief; summarize them as local protected inputs and set redacted=true.",
|
|
1897
1914
|
`ontologyVersion must be exactly ${WORKFORCE_ONTOLOGY_VERSION}.`,
|
|
1898
|
-
|
|
1915
|
+
WORKFORCE_ONTOLOGY_GUIDE,
|
|
1899
1916
|
searchSchemaRequirements,
|
|
1900
1917
|
].join("\n"),
|
|
1901
1918
|
searchUser: task,
|
|
@@ -1906,12 +1923,12 @@ function buildPrompts(task, identity) {
|
|
|
1906
1923
|
"Return a complete replacement WorkOrder authored by you. Preserve workOrderId and the redacted taskBrief exactly. Preserve every genuinely essential responsibility; add or separate an omitted accountable domain job family when the task requires it.",
|
|
1907
1924
|
"Any specialized domain explicitly present in the task with distinct failure or accountability semantics must remain or become its own accountable domain slot. Examples include payments, insurance, legal, finance, travel, and regulated science or operations. Never collapse one into generic backend, software, database, or implementation work.",
|
|
1908
1925
|
"Reconsider only hard eligibility gates exposed by the gap codes. gap:excluded:missing-required-skill means reassess requiredSkills and move desired expertise to optionalSkills/task unless exact profile proof is execution-essential. gap:excluded:missing-required-tool means remove or revise requiredToolCapabilities unless the worker itself must invoke that exact host tool. gap:excluded:missing-consumed-artifact and gap:excluded:missing-produced-artifact mean move normal workflow inputs/outputs to task or edges unless the candidate profile itself must declare that exact artifact. gap:excluded:entity-kind-mismatch means reconsider allowedEntityKinds and permit executable agent or team when it can own the accountability; never select group. gap:selection-requested-content-expansion means revisit the responsibility and semantic job-family description without reading candidate identities or content.",
|
|
1909
|
-
"
|
|
1926
|
+
"Do not author requiredRoles, requiredToolCapabilities, requiredAuthorities, forbiddenAuthorities, consumes, produces, or modalities; move desired fit to title, task, optionalCommunities, or optionalSkills — ordinary handoffs belong in task and edges.",
|
|
1910
1927
|
"Preserve community prohibitions explicitly stated in the redacted taskBrief. You may correct exclusions inferred by the prior job analysis when they conflict with required/optional job-family lineage or when coverage gap codes show forbidden-community exclusion. Never turn forbiddenCommunities or excludedCommunities into an exhaustive list of unused families, and never forbid a broad, adjacent, or legitimately co-occurring community merely to sharpen a slot.",
|
|
1911
1928
|
"Before returning JSON, self-check that each explicitly named specialized domain responsibility has an independent accountable slot and that every hard gate still satisfies the execution-impossible or exact-profile-declaration test.",
|
|
1912
1929
|
"The host will validate your replacement exactly and will not add slots, defaults, constraints, candidates, or substitutions. At most two total semantic WorkOrder refinements are allowed.",
|
|
1913
1930
|
`ontologyVersion must remain exactly ${WORKFORCE_ONTOLOGY_VERSION}.`,
|
|
1914
|
-
|
|
1931
|
+
WORKFORCE_ONTOLOGY_GUIDE,
|
|
1915
1932
|
searchSchemaRequirements,
|
|
1916
1933
|
].join("\n"),
|
|
1917
1934
|
selectionSystem: [
|
|
@@ -42,9 +42,14 @@ function cloudCasResponseError(response, label) {
|
|
|
42
42
|
let message = `${label} failed with HTTP ${response.status}`;
|
|
43
43
|
if (response.status === 412 && code === "cloud_agent_revision_conflict") {
|
|
44
44
|
const current = body && body.current ? body.current : body && body.conflict && body.conflict.current;
|
|
45
|
+
// 서버가 알려준 사실만 사람 말로 전달한다. 내부 개념(precondition, base, 연결)은
|
|
46
|
+
// 사용자에게 아무 의미가 없다 — 기준은 "이 이름이 내 계정 것인가" 하나다.
|
|
45
47
|
message = current
|
|
46
|
-
?
|
|
47
|
-
|
|
48
|
+
? `업로드하지 않았습니다. "${current.slug || "이 이름"}"의 서버 버전이 지금 보낸 것과 맞지 않습니다` +
|
|
49
|
+
`${current.updatedAt ? ` (서버 쪽 마지막 저장: ${current.updatedAt})` : ""}.\n` +
|
|
50
|
+
` 내 계정 자산이면: agentlas cloud restore ${current.slug || "<slug>"} 로 받아서 확인한 뒤 다시 저장하세요.\n` +
|
|
51
|
+
` (서버 cloudId ${current.cloudId || "?"} · revision ${current.revision || "?"})`
|
|
52
|
+
: "업로드하지 않았습니다. 이 이름의 자산이 삭제됐거나 다른 계정 소유입니다. `agentlas cloud list`로 내 자산을 확인하세요.";
|
|
48
53
|
} else if (response.status === 428 && code === "client_upgrade_required") {
|
|
49
54
|
message = "No base revision is available to safely update the existing Cloud asset. The server revision will not be copied automatically. Check `agentlas cloud list`, restore with `agentlas cloud restore <slug>`, then save again.";
|
|
50
55
|
} else if (response.status === 503 && code === "cloud_mutations_maintenance") {
|
|
@@ -10,12 +10,161 @@
|
|
|
10
10
|
* 명시적 --visibility marketplace 로만 publish가 된다.
|
|
11
11
|
* 오류는 throw → 명령 파일이 ctx.err + return 1 로 처리한다 (process.exit 금지).
|
|
12
12
|
*/
|
|
13
|
+
const fs = require("node:fs");
|
|
14
|
+
const os = require("node:os");
|
|
15
|
+
const path = require("node:path");
|
|
16
|
+
const readline = require("node:readline/promises");
|
|
13
17
|
const cloudRuntime = require("../agentlas-cloud-runtime.cjs");
|
|
14
18
|
const { installHubAgent } = require("../hub/install.cjs");
|
|
15
19
|
const { callHubTool, HubError } = require("../cloud/hub-client.cjs");
|
|
16
20
|
const { packageCloudAgent } = require("./package.cjs");
|
|
17
21
|
const { deleteCloudAgent } = require("./cas.cjs");
|
|
18
22
|
const { listOwnedCloudAgents, restoreOwnedCloudAgent } = require("./restore.cjs");
|
|
23
|
+
const workforceDeps = require("../workforce/deps.cjs");
|
|
24
|
+
const workforceCapture = require("../workforce/capture.cjs");
|
|
25
|
+
|
|
26
|
+
const PURPOSE_QUESTION = "What concrete work should this agent complete, and what should the finished result look like?";
|
|
27
|
+
|
|
28
|
+
function purposeRepairNeeded(result) {
|
|
29
|
+
const ids = new Set((result?.review?.findings || []).map((finding) => finding.id));
|
|
30
|
+
return ids.has("routing-card-required") ||
|
|
31
|
+
(result?.review?.findings || []).some((finding) =>
|
|
32
|
+
finding.id === "routing-card-invalid" && /summary|capabilities|name|id|workforce/i.test(finding.message || ""));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function askPurpose(ctx, flags) {
|
|
36
|
+
if (typeof flags.purpose === "string" && flags.purpose.trim()) return flags.purpose.trim();
|
|
37
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return "";
|
|
38
|
+
const prompt = ctx.lang === "ko"
|
|
39
|
+
? "이 에이전트가 실제로 끝내야 할 일과, 완료됐을 때 나와야 할 결과를 평소 말하듯 적어주세요: "
|
|
40
|
+
: `${PURPOSE_QUESTION} `;
|
|
41
|
+
const terminal = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
42
|
+
try {
|
|
43
|
+
return String(await terminal.question(prompt)).trim();
|
|
44
|
+
} finally {
|
|
45
|
+
terminal.close();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parsePurposeProjection(raw) {
|
|
50
|
+
const text = String(raw || "").trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
|
|
51
|
+
const value = JSON.parse(text);
|
|
52
|
+
const ids = (input, prefix, min = 1) => {
|
|
53
|
+
if (!Array.isArray(input)) throw new Error(`${prefix} must be an array`);
|
|
54
|
+
const pattern = prefix === "capability"
|
|
55
|
+
? /^[a-z][a-z0-9]*(_[a-z0-9]+)+$/
|
|
56
|
+
: new RegExp(`^${prefix}:[a-z0-9][a-z0-9-]*$`);
|
|
57
|
+
const output = [...new Set(input.map((item) => String(item).trim()).filter((item) => pattern.test(item)))];
|
|
58
|
+
if (output.length < min) throw new Error(`${prefix} is incomplete`);
|
|
59
|
+
return output;
|
|
60
|
+
};
|
|
61
|
+
const requiredText = (key) => {
|
|
62
|
+
const output = typeof value[key] === "string" ? value[key].trim() : "";
|
|
63
|
+
if (!output) throw new Error(`${key} is missing`);
|
|
64
|
+
return output;
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
titleEn: requiredText("titleEn"),
|
|
68
|
+
titleKo: requiredText("titleKo"),
|
|
69
|
+
summary: requiredText("summary"),
|
|
70
|
+
descriptionKo: requiredText("descriptionKo"),
|
|
71
|
+
capabilities: ids(value.capabilities, "capability", 2),
|
|
72
|
+
roles: ids(value.roles, "role"),
|
|
73
|
+
communities: ids(value.communities, "community"),
|
|
74
|
+
skills: ids(value.skills, "skill", 2),
|
|
75
|
+
knowledge: ids(value.knowledge, "knowledge"),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function projectPurposeWithConnectedModel(ctx, answer, fallbackName) {
|
|
80
|
+
const runtime = workforceDeps.resolveWorkforceRuntime(ctx.db()).roleRuntimes.orchestrator;
|
|
81
|
+
const system = [
|
|
82
|
+
"Convert an ordinary-language agent purpose into one English internal routing resume.",
|
|
83
|
+
"The answer may be in any language. Preserve meaning; do not add tools, runtimes, languages, modalities, permissions, authorities, or forbidden authorities.",
|
|
84
|
+
"Return JSON only.",
|
|
85
|
+
].join(" ");
|
|
86
|
+
const prompt = [
|
|
87
|
+
`Working name: ${fallbackName}`,
|
|
88
|
+
`User explanation: ${answer}`,
|
|
89
|
+
"",
|
|
90
|
+
'Return {"titleEn":"","titleKo":"","summary":"","descriptionKo":"","capabilities":[],"roles":[],"communities":[],"skills":[],"knowledge":[]}.',
|
|
91
|
+
"summary must be a concrete English description of work and finished result.",
|
|
92
|
+
"capabilities are 2-8 English snake_case verb-object phrases.",
|
|
93
|
+
"roles, communities, skills, knowledge are faithful open-world English IDs with role:, community:, skill:, knowledge: prefixes.",
|
|
94
|
+
"titleKo and descriptionKo are display translations only; every routing field remains English.",
|
|
95
|
+
].join("\n");
|
|
96
|
+
let raw;
|
|
97
|
+
if (runtime.mode === "api") {
|
|
98
|
+
raw = await workforceCapture.runApi(runtime.backend, runtime.model, system, prompt, {});
|
|
99
|
+
} else {
|
|
100
|
+
if (runtime.kind === "gemini") {
|
|
101
|
+
throw new Error("The connected Gemini CLI cannot run this no-tool repair safely yet. Choose Codex/Claude or a BYOK model and retry.");
|
|
102
|
+
}
|
|
103
|
+
const env = await workforceCapture.buildChildEnv(ctx.db(), { cwd: process.cwd(), projectPath: process.cwd() });
|
|
104
|
+
raw = await workforceCapture.captureRuntime(runtime.kind, system, prompt, {
|
|
105
|
+
cwd: workforceCapture.runCwd(),
|
|
106
|
+
env,
|
|
107
|
+
permission: "read",
|
|
108
|
+
authorityMode: "no-authority",
|
|
109
|
+
model: runtime.model,
|
|
110
|
+
effort: runtime.effort,
|
|
111
|
+
timeoutConfig: { connectMs: 30_000, idleMs: 45_000, totalMs: 90_000 },
|
|
112
|
+
outputLimitBytes: 256 * 1024,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return parsePurposeProjection(raw);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function writePurposeRepairCopy(root, projection) {
|
|
119
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "agentlas-purpose-repair-"));
|
|
120
|
+
fs.cpSync(path.resolve(root), tempRoot, { recursive: true, dereference: false });
|
|
121
|
+
const metadataDir = path.join(tempRoot, ".agentlas");
|
|
122
|
+
fs.mkdirSync(metadataDir, { recursive: true });
|
|
123
|
+
const routingPath = path.join(metadataDir, "routing-card.json");
|
|
124
|
+
let routing = {};
|
|
125
|
+
try { routing = JSON.parse(fs.readFileSync(routingPath, "utf8")); } catch { /* construct it */ }
|
|
126
|
+
const id = String(routing.id || path.basename(root))
|
|
127
|
+
.normalize("NFKC").toLowerCase().replace(/[^a-z0-9._/-]+/g, "-").replace(/^-|-$/g, "") || "agent";
|
|
128
|
+
const card = {
|
|
129
|
+
...routing,
|
|
130
|
+
schemaVersion: "routing-card/2.0",
|
|
131
|
+
id,
|
|
132
|
+
type: routing.type === "team" || routing.type === "plugin" ? routing.type : "agent",
|
|
133
|
+
name: projection.titleEn,
|
|
134
|
+
summary: projection.summary.slice(0, 240),
|
|
135
|
+
capabilities: projection.capabilities,
|
|
136
|
+
routing_status: "routing_ready",
|
|
137
|
+
workforce: {
|
|
138
|
+
...(routing.workforce && typeof routing.workforce === "object" && !Array.isArray(routing.workforce)
|
|
139
|
+
? routing.workforce
|
|
140
|
+
: {}),
|
|
141
|
+
roles: projection.roles,
|
|
142
|
+
communities: projection.communities,
|
|
143
|
+
skills: projection.skills,
|
|
144
|
+
knowledge: projection.knowledge,
|
|
145
|
+
languages: [],
|
|
146
|
+
modalities: [],
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
fs.writeFileSync(routingPath, `${JSON.stringify(card, null, 2)}\n`, "utf8");
|
|
150
|
+
const agentCardPath = path.join(metadataDir, "agent-card.json");
|
|
151
|
+
let agentCard = {};
|
|
152
|
+
try { agentCard = JSON.parse(fs.readFileSync(agentCardPath, "utf8")); } catch { /* construct it */ }
|
|
153
|
+
agentCard = {
|
|
154
|
+
...agentCard,
|
|
155
|
+
name: agentCard.name || projection.titleEn,
|
|
156
|
+
summary: projection.summary,
|
|
157
|
+
localized: {
|
|
158
|
+
...(agentCard.localized && typeof agentCard.localized === "object" ? agentCard.localized : {}),
|
|
159
|
+
titleEn: projection.titleEn,
|
|
160
|
+
titleKo: projection.titleKo,
|
|
161
|
+
descriptionEn: projection.summary,
|
|
162
|
+
descriptionKo: projection.descriptionKo,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
fs.writeFileSync(agentCardPath, `${JSON.stringify(agentCard, null, 2)}\n`, "utf8");
|
|
166
|
+
return tempRoot;
|
|
167
|
+
}
|
|
19
168
|
|
|
20
169
|
function parseCloudFlags(args) {
|
|
21
170
|
const flags = { _: [] };
|
|
@@ -102,7 +251,9 @@ const CLOUD_HELP = [
|
|
|
102
251
|
" save owner-private in Agent Cloud (default upload)",
|
|
103
252
|
" publish <path> [--dry-run] [--slug name]",
|
|
104
253
|
" explicitly publish to the public Agentlas Hub",
|
|
254
|
+
" --purpose \"ordinary explanation\" repair a missing purpose through your connected model",
|
|
105
255
|
" package <path> [--json] [--visibility private-link|marketplace]",
|
|
256
|
+
" --overwrite 서버에 더 새 버전이 있어도 지금 폴더 내용으로 덮어쓰기",
|
|
106
257
|
" package only; defaults to private-save checks",
|
|
107
258
|
" list [--json] list packages in your private Agent Cloud",
|
|
108
259
|
" restore <slug> [--json] restore an owned Cloud package on this machine",
|
|
@@ -237,12 +388,30 @@ async function runCloud(ctx, args) {
|
|
|
237
388
|
if (!root) { ctx.err(`usage: agentlas cloud ${sub} <path>`); return 1; }
|
|
238
389
|
const visibility = cloudVisibilityForAction(sub, flags);
|
|
239
390
|
const dryRun = sub === "package" || Boolean(flags["dry-run"]);
|
|
240
|
-
const
|
|
391
|
+
const packageOptions = {
|
|
241
392
|
slug: typeof flags.slug === "string" ? flags.slug : undefined,
|
|
242
393
|
visibility,
|
|
243
394
|
llmReview: Boolean(flags["llm-review"]),
|
|
395
|
+
overwriteRemote: Boolean(flags.overwrite),
|
|
244
396
|
dryRun,
|
|
245
|
-
}
|
|
397
|
+
};
|
|
398
|
+
let result = await packageCloudAgent(ctx.db(), root, packageOptions);
|
|
399
|
+
if (sub === "publish" && result.status === "blocked" && purposeRepairNeeded(result) && !flags.json) {
|
|
400
|
+
const answer = await askPurpose(ctx, flags);
|
|
401
|
+
if (answer) {
|
|
402
|
+
const projection = await projectPurposeWithConnectedModel(ctx, answer, path.basename(path.resolve(root)));
|
|
403
|
+
const repairedRoot = writePurposeRepairCopy(root, projection);
|
|
404
|
+
try {
|
|
405
|
+
result = await packageCloudAgent(ctx.db(), repairedRoot, packageOptions);
|
|
406
|
+
} finally {
|
|
407
|
+
fs.rmSync(repairedRoot, { recursive: true, force: true });
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
ctx.err(ctx.lang === "ko"
|
|
411
|
+
? `업로드를 완성하려면 답이 필요합니다. 다시 실행할 때 --purpose "평소 말로 설명"을 붙이세요.`
|
|
412
|
+
: `This upload needs one answer. Retry with --purpose "your ordinary explanation".`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
246
415
|
if (flags.json) {
|
|
247
416
|
ctx.out(JSON.stringify(result, null, 2));
|
|
248
417
|
return (sub === "save" || sub === "publish") && result.status === "blocked" ? 1 : 0;
|
|
@@ -267,6 +436,8 @@ module.exports = {
|
|
|
267
436
|
cloudVisibilityFlag,
|
|
268
437
|
cloudVisibilityForAction,
|
|
269
438
|
cloudActionForTopLevelUpload,
|
|
439
|
+
purposeRepairNeeded,
|
|
440
|
+
parsePurposeProjection,
|
|
270
441
|
printCloudPackageResult,
|
|
271
442
|
runCloud,
|
|
272
443
|
runUpload,
|
|
@@ -33,10 +33,12 @@ const {
|
|
|
33
33
|
cloudPortablePathConflict,
|
|
34
34
|
cloudPackageHashVersion,
|
|
35
35
|
cloudHashPackage,
|
|
36
|
+
normalizeCloudAssetDescriptor,
|
|
36
37
|
} = require("../hub/install.cjs");
|
|
37
38
|
const { SECRET_PATTERNS } = require("../agentlas-secret-patterns.cjs");
|
|
38
39
|
const { userDataDir } = require("../core/paths.cjs");
|
|
39
40
|
const state = require("./state.cjs");
|
|
41
|
+
const { cargoSearchAgents } = require("./cargo.cjs");
|
|
40
42
|
const cas = require("./cas.cjs");
|
|
41
43
|
|
|
42
44
|
const CLOUD_TEXT_EXTS = new Set([".cfg", ".cjs", ".conf", ".config", ".css", ".csv", ".env", ".html", ".ini", ".js", ".json", ".jsonl", ".md", ".mjs", ".properties", ".ps1", ".psd1", ".psm1", ".py", ".sh", ".toml", ".ts", ".tsx", ".txt", ".xml", ".yaml", ".yml"]);
|
|
@@ -575,6 +577,7 @@ function cloudRoutingCardProblem(card) {
|
|
|
575
577
|
if (card.type !== "agent" && card.type !== "team" && card.type !== "plugin") return "type must be agent, team, or plugin";
|
|
576
578
|
if (typeof card.name !== "string" || !card.name.trim()) return "name must be a non-empty string";
|
|
577
579
|
if (typeof card.summary !== "string" || !card.summary.trim()) return "summary must be a non-empty string";
|
|
580
|
+
if (card.summary.length > 240) return "summary must be at most 240 characters";
|
|
578
581
|
if (!Array.isArray(card.capabilities) || card.capabilities.length === 0) return "capabilities must be a non-empty array";
|
|
579
582
|
for (const capability of card.capabilities) {
|
|
580
583
|
if (typeof capability !== "string" || !CLOUD_ROUTING_CARD_CAPABILITY_RE.test(capability)) {
|
|
@@ -584,6 +587,33 @@ function cloudRoutingCardProblem(card) {
|
|
|
584
587
|
if (typeof card.routing_status !== "string" || !CLOUD_ROUTING_CARD_STATUSES.has(card.routing_status)) {
|
|
585
588
|
return "routing_status must be draft, searchable, candidate, routing_ready, or trusted";
|
|
586
589
|
}
|
|
590
|
+
const workforce = card.workforce;
|
|
591
|
+
if (!workforce || typeof workforce !== "object" || Array.isArray(workforce)) {
|
|
592
|
+
return "workforce must be a complete semantic resume";
|
|
593
|
+
}
|
|
594
|
+
const semanticLists = [
|
|
595
|
+
["communities", /^community:[a-z0-9][a-z0-9-]*$/, 1, 5],
|
|
596
|
+
["roles", /^role:[a-z0-9][a-z0-9-]*$/, 0, 4],
|
|
597
|
+
["skills", /^skill:[a-z0-9][a-z0-9-]*$/, 1, 12],
|
|
598
|
+
["knowledge", /^knowledge:[a-z0-9][a-z0-9-]*$/, 0, 256],
|
|
599
|
+
];
|
|
600
|
+
for (const [field, pattern, minimum, maximum] of semanticLists) {
|
|
601
|
+
const values = workforce[field];
|
|
602
|
+
if (!Array.isArray(values) || values.length < minimum || values.length > maximum) {
|
|
603
|
+
return `workforce.${field} must contain ${minimum}-${maximum} English semantic IDs`;
|
|
604
|
+
}
|
|
605
|
+
if (new Set(values).size !== values.length ||
|
|
606
|
+
values.some((value) => typeof value !== "string" || !pattern.test(value))) {
|
|
607
|
+
return `workforce.${field} contains an invalid or duplicate semantic ID`;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
for (const field of ["languages", "modalities"]) {
|
|
611
|
+
const values = workforce[field];
|
|
612
|
+
if (!Array.isArray(values) || new Set(values).size !== values.length ||
|
|
613
|
+
values.some((value) => typeof value !== "string")) {
|
|
614
|
+
return `workforce.${field} must be a unique string array`;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
587
617
|
return null;
|
|
588
618
|
}
|
|
589
619
|
|
|
@@ -748,6 +778,28 @@ function cloudSecuritySummary(findings) {
|
|
|
748
778
|
|
|
749
779
|
// ── 메인: 패키지(+등록) ──
|
|
750
780
|
|
|
781
|
+
/**
|
|
782
|
+
* 이 계정이 소유한 자산의 현재 리비전을 서버에서 조회한다.
|
|
783
|
+
* 로컬 관측 기록이 전혀 없는 새 PC/새 클론에서 갱신을 가능하게 하는 유일한 경로다.
|
|
784
|
+
* 소유자 세션이 없거나 자산이 없으면 null — 조용한 폴백은 만들지 않는다.
|
|
785
|
+
*/
|
|
786
|
+
async function lookupOwnedCloudDescriptor(slug, scope) {
|
|
787
|
+
let response;
|
|
788
|
+
try {
|
|
789
|
+
response = await cargoSearchAgents({ q: slug, limit: 20 });
|
|
790
|
+
} catch {
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
const rows = Array.isArray(response && response.results) ? response.results : [];
|
|
794
|
+
const row = rows.find((item) => item && item.slug === slug && item.scope === scope);
|
|
795
|
+
if (!row) return null;
|
|
796
|
+
try {
|
|
797
|
+
return normalizeCloudAssetDescriptor(row, "owner cloud search result");
|
|
798
|
+
} catch {
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
751
803
|
async function packageCloudAgent(db, root, opts = {}) {
|
|
752
804
|
const requestedRoot = path.resolve(root);
|
|
753
805
|
let st;
|
|
@@ -796,7 +848,7 @@ async function packageCloudAgent(db, root, opts = {}) {
|
|
|
796
848
|
const name = cloudReadName(snapshot, path.basename(rootPath));
|
|
797
849
|
const slug = cloudSlug(opts.slug || cloudReadStableSlug(snapshot) || name || path.basename(rootPath));
|
|
798
850
|
const scope = cas.cloudScopeForVisibility(visibility);
|
|
799
|
-
|
|
851
|
+
let baseDescriptor = state.cloudBaseDescriptorForSource(scan.localPackageMarker, rootPath, slug, scope);
|
|
800
852
|
const packageHashVersion = CLOUD_PACKAGE_HASH_V2;
|
|
801
853
|
const packageHash = cloudHashPackage(scan.included, packageHashVersion);
|
|
802
854
|
const manifest = {
|
|
@@ -844,6 +896,30 @@ async function packageCloudAgent(db, root, opts = {}) {
|
|
|
844
896
|
let registration = null;
|
|
845
897
|
let status = blocked ? "blocked" : opts.dryRun ? "dry-run" : "ready";
|
|
846
898
|
if (!blocked && !opts.dryRun) {
|
|
899
|
+
// 자산의 정체성은 (로그인 계정, slug)다 — 어느 폴더에서 올리는지는 중요하지 않다.
|
|
900
|
+
// 쓰기 전에 서버가 들고 있는 내 자산의 현재 버전을 조회해서:
|
|
901
|
+
// · 이 폴더에 기록이 없으면(새 PC, 새 클론) 그 버전을 기준으로 업데이트한다.
|
|
902
|
+
// · 이 폴더 기록이 서버보다 오래됐으면(다른 곳에서 먼저 올림) 덮어쓰기 전에 멈춘다.
|
|
903
|
+
// 이것은 412 뒤의 자동 재시도가 아니라 쓰기 전 refresh다. 등록 자체는 여전히
|
|
904
|
+
// 조회한 정확한 리비전에 대한 조건부 쓰기(If-Match)라 경합은 서버가 막는다.
|
|
905
|
+
const remote = await lookupOwnedCloudDescriptor(slug, scope);
|
|
906
|
+
if (!baseDescriptor && remote) {
|
|
907
|
+
baseDescriptor = state.rememberCloudAssetDescriptor(remote, { sourceRoot: rootPath });
|
|
908
|
+
} else if (baseDescriptor && remote && baseDescriptor.revision !== remote.revision) {
|
|
909
|
+
if (opts.overwriteRemote) {
|
|
910
|
+
baseDescriptor = state.rememberCloudAssetDescriptor(remote, { sourceRoot: rootPath });
|
|
911
|
+
} else {
|
|
912
|
+
const error = new Error(
|
|
913
|
+
`업로드하지 않았습니다. "${slug}"에 더 새 버전이 있습니다 (${remote.updatedAt || "시각 미상"}에 다른 곳에서 저장됨).\n` +
|
|
914
|
+
` 지금 폴더 내용으로 그 버전을 덮어쓰려면: --overwrite\n` +
|
|
915
|
+
` 먼저 그 버전을 받아서 비교하려면: agentlas cloud restore ${slug}\n` +
|
|
916
|
+
` (서버 cloudId ${remote.cloudId} · revision ${remote.revision})`,
|
|
917
|
+
);
|
|
918
|
+
error.code = "cloud_agent_revision_conflict";
|
|
919
|
+
error.current = remote;
|
|
920
|
+
throw error;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
847
923
|
registration = await cas.registerCloudAgent(manifest, bundlePath, review, visibility, { baseDescriptor });
|
|
848
924
|
let descriptor;
|
|
849
925
|
try {
|
|
@@ -174,6 +174,39 @@ function cloudBaseDescriptorFromMarker(marker, slug, scope) {
|
|
|
174
174
|
* 톰스톤에 걸린 마커 디스크립터는 무시(삭제 후 재저장은 새 생성이어야 한다).
|
|
175
175
|
* 둘 다 있으면 같은 cloudId에서 더 최신 관측을 채택한다.
|
|
176
176
|
*/
|
|
177
|
+
/**
|
|
178
|
+
* 이 slug/scope의 관측된 리비전은 있는데, 이 소스 루트에는 아직 연결되지 않은
|
|
179
|
+
* 경우를 돌려준다.
|
|
180
|
+
*
|
|
181
|
+
* 이 구분이 없으면 base 조회가 null을 돌려주고, 호출부는 그것을 "새 자산"으로
|
|
182
|
+
* 읽어 create precondition을 보낸다. 서버에는 자산이 멀쩡히 존재하므로 412로
|
|
183
|
+
* 거절되고, 사용자는 "다른 PC에서 변경됨 / restore 하세요"라는 엉뚱한 안내를
|
|
184
|
+
* 받는다. restore는 디스크립터를 설치 경로에 묶으므로 소스 루트에서 발행하는
|
|
185
|
+
* 한 영원히 해소되지 않는다. 모름을 그럴듯한 값으로 메꾸지 않기 위한 분기다.
|
|
186
|
+
*/
|
|
187
|
+
function cloudUnboundDescriptorForSource(rootPath, slug, scope) {
|
|
188
|
+
const state = readCloudAssetState();
|
|
189
|
+
const entry = state.assets[`${scope}:${slug}`];
|
|
190
|
+
if (!entry) return null;
|
|
191
|
+
return entry.sourceRoots.includes(path.resolve(rootPath)) ? null : entry.descriptor;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** 이 소스 루트를 이미 관측된 클라우드 자산에 명시적으로 연결한다. */
|
|
195
|
+
function bindCloudAssetSourceRoot(rootPath, slug, scope) {
|
|
196
|
+
const state = readCloudAssetState();
|
|
197
|
+
const key = `${scope}:${slug}`;
|
|
198
|
+
const entry = state.assets[key];
|
|
199
|
+
if (!entry) throw new Error(`No observed Cloud revision for ${key} to bind.`);
|
|
200
|
+
const normalizedRoot = path.resolve(rootPath);
|
|
201
|
+
const roots = [...new Set([...entry.sourceRoots, normalizedRoot])].slice(0, 32);
|
|
202
|
+
state.assets[key] = { descriptor: entry.descriptor, sourceRoots: roots };
|
|
203
|
+
state.deletedBases = state.deletedBases.filter(
|
|
204
|
+
(item) => !(item.rootPath === normalizedRoot && item.slug === slug && item.scope === scope),
|
|
205
|
+
);
|
|
206
|
+
writeCloudAssetState(state);
|
|
207
|
+
return entry.descriptor;
|
|
208
|
+
}
|
|
209
|
+
|
|
177
210
|
function cloudBaseDescriptorForSource(marker, rootPath, slug, scope) {
|
|
178
211
|
const state = readCloudAssetState();
|
|
179
212
|
const normalizedRoot = path.resolve(rootPath);
|
|
@@ -263,6 +296,8 @@ module.exports = {
|
|
|
263
296
|
cloudMarkerDescriptors,
|
|
264
297
|
cloudBaseDescriptorFromMarker,
|
|
265
298
|
cloudBaseDescriptorForSource,
|
|
299
|
+
cloudUnboundDescriptorForSource,
|
|
300
|
+
bindCloudAssetSourceRoot,
|
|
266
301
|
writeCloudSourceMarker,
|
|
267
302
|
readCloudSourceMarker,
|
|
268
303
|
};
|
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
installPluginMcpRows,
|
|
13
13
|
listHubPlugins,
|
|
14
14
|
} = require("../hub/plugins.cjs");
|
|
15
|
+
const { terminalProjectCandidateCli, initializedAgentlasProjectPathCli } = require("../project/state.cjs");
|
|
15
16
|
|
|
16
17
|
async function pluginAdd(ctx, slug) {
|
|
17
18
|
if (!slug) {
|
|
@@ -90,12 +91,90 @@ async function pluginList(ctx) {
|
|
|
90
91
|
return 0;
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
async function pluginListWithProject(ctx, projectPath) {
|
|
95
|
+
const root = terminalProjectCandidateCli(projectPath);
|
|
96
|
+
if (!root) {
|
|
97
|
+
ctx.err(`project path is invalid or not safe: ${projectPath}`);
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
if (!initializedAgentlasProjectPathCli(root)) {
|
|
101
|
+
ctx.err("project is not initialized for plugin listing. Run `agentlas project init` in that folder first.");
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
ctx.out("Scope: global Hub plugin catalog (project compatibility and local installation state are not evaluated).");
|
|
105
|
+
return pluginList(ctx);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parsePluginListFlags(args) {
|
|
109
|
+
const flags = {
|
|
110
|
+
global: false,
|
|
111
|
+
project: null,
|
|
112
|
+
errors: [],
|
|
113
|
+
positionals: [],
|
|
114
|
+
};
|
|
115
|
+
for (let i = 0; i < args.length; i++) {
|
|
116
|
+
const arg = args[i];
|
|
117
|
+
if (arg === "--global") {
|
|
118
|
+
flags.global = true;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (arg === "--project") {
|
|
122
|
+
const next = args[i + 1];
|
|
123
|
+
if (!next || next.startsWith("-")) {
|
|
124
|
+
flags.errors.push("usage: agentlas plugin list --project <path>");
|
|
125
|
+
return flags;
|
|
126
|
+
}
|
|
127
|
+
flags.project = next;
|
|
128
|
+
i += 1;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (arg === "--help" || arg === "-h") {
|
|
132
|
+
flags.errors.push("usage: agentlas plugin list [--global | --project <path>]");
|
|
133
|
+
return flags;
|
|
134
|
+
}
|
|
135
|
+
if (arg.startsWith("--")) {
|
|
136
|
+
flags.errors.push(`unknown flag: ${arg}`);
|
|
137
|
+
return flags;
|
|
138
|
+
}
|
|
139
|
+
flags.positionals.push(arg);
|
|
140
|
+
}
|
|
141
|
+
if (flags.project && flags.global) {
|
|
142
|
+
flags.errors.push("cannot use --project and --global together");
|
|
143
|
+
}
|
|
144
|
+
if (flags.positionals.length > 0) {
|
|
145
|
+
flags.errors.push("usage: agentlas plugin list [--global | --project <path>]");
|
|
146
|
+
}
|
|
147
|
+
return flags;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function runPluginList(ctx, args) {
|
|
151
|
+
const flags = parsePluginListFlags(args);
|
|
152
|
+
if (flags.errors.length) {
|
|
153
|
+
ctx.err(flags.errors[0]);
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
if (flags.project) return pluginListWithProject(ctx, flags.project);
|
|
157
|
+
return pluginList(ctx);
|
|
158
|
+
}
|
|
159
|
+
|
|
93
160
|
function run(ctx, args) {
|
|
94
161
|
const action = args[0];
|
|
95
|
-
if (action === "add")
|
|
96
|
-
|
|
162
|
+
if (action === "add") {
|
|
163
|
+
if (args.length !== 2) {
|
|
164
|
+
ctx.err("usage: agentlas plugin add <slug> (run agentlas plugin list first)");
|
|
165
|
+
return 1;
|
|
166
|
+
}
|
|
167
|
+
return pluginAdd(ctx, args[1]);
|
|
168
|
+
}
|
|
169
|
+
if (action === "list" || !action) {
|
|
170
|
+
const rest = action === "list" ? args.slice(1) : [];
|
|
171
|
+
return runPluginList(ctx, rest);
|
|
172
|
+
}
|
|
97
173
|
ctx.err("usage: agentlas plugin add <slug> | agentlas plugin list");
|
|
98
174
|
return 1;
|
|
99
175
|
}
|
|
100
176
|
|
|
101
|
-
module.exports = {
|
|
177
|
+
module.exports = {
|
|
178
|
+
run,
|
|
179
|
+
parsePluginListFlags,
|
|
180
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlas",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
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"
|