agentlas 0.9.10 → 1.0.2

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.
Files changed (145) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/README.md +319 -341
  3. package/bin/agentlas.cjs +32 -9
  4. package/engine/agentlas-banner.cjs +24 -3
  5. package/engine/agentlas-composer.cjs +239 -31
  6. package/engine/agentlas-config.cjs +103 -7
  7. package/engine/agentlas-core-harness.cjs +48 -4
  8. package/engine/agentlas-evolution.cjs +3 -4
  9. package/engine/agentlas-i18n.cjs +88 -32
  10. package/engine/agentlas-input.cjs +234 -18
  11. package/engine/agentlas-memory-governance.cjs +3 -5
  12. package/engine/agentlas-memory-import.cjs +3 -3
  13. package/engine/agentlas-native-host.cjs +200 -9
  14. package/engine/agentlas-onboard.cjs +112 -20
  15. package/engine/agentlas-permissions.cjs +14 -7
  16. package/engine/agentlas-sqlite-policy.cjs +34 -0
  17. package/engine/agentlas-ui.cjs +93 -42
  18. package/engine/agentlas-workforce.cjs +472 -58
  19. package/engine/agentlas-workload-routing.cjs +8 -3
  20. package/engine/agentlas.cjs +140 -12367
  21. package/engine/agents/files.cjs +61 -0
  22. package/engine/agents/import-local.cjs +237 -0
  23. package/engine/agents/registry.cjs +158 -0
  24. package/engine/agents/router.cjs +565 -0
  25. package/engine/agents/routes.cjs +43 -0
  26. package/engine/architecture.data.json +2 -2
  27. package/engine/automation/daemon.cjs +335 -0
  28. package/engine/automation/schedule.cjs +181 -0
  29. package/engine/automation/store.cjs +209 -0
  30. package/engine/cloud/auth.cjs +279 -0
  31. package/engine/cloud/hub-client.cjs +239 -0
  32. package/engine/cloud-assets/cargo.cjs +49 -0
  33. package/engine/cloud-assets/cas.cjs +235 -0
  34. package/engine/cloud-assets/commands.cjs +273 -0
  35. package/engine/cloud-assets/package.cjs +936 -0
  36. package/engine/cloud-assets/restore.cjs +172 -0
  37. package/engine/cloud-assets/state.cjs +268 -0
  38. package/engine/commands/automation.cjs +195 -0
  39. package/engine/commands/billing.cjs +96 -0
  40. package/engine/commands/browser.cjs +20 -0
  41. package/engine/commands/build.cjs +33 -0
  42. package/engine/commands/call.cjs +24 -0
  43. package/engine/commands/career-graph.cjs +51 -0
  44. package/engine/commands/cd.cjs +22 -0
  45. package/engine/commands/chat.cjs +12 -0
  46. package/engine/commands/chats.cjs +28 -0
  47. package/engine/commands/cloud.cjs +17 -0
  48. package/engine/commands/connect.cjs +20 -0
  49. package/engine/commands/context.cjs +66 -0
  50. package/engine/commands/creds.cjs +203 -0
  51. package/engine/commands/doctor.cjs +68 -0
  52. package/engine/commands/env.cjs +33 -0
  53. package/engine/commands/evolve.cjs +23 -0
  54. package/engine/commands/experience.cjs +34 -0
  55. package/engine/commands/film.cjs +8 -0
  56. package/engine/commands/firm.cjs +115 -0
  57. package/engine/commands/help.cjs +65 -0
  58. package/engine/commands/hep.cjs +23 -0
  59. package/engine/commands/import.cjs +35 -0
  60. package/engine/commands/index.cjs +136 -0
  61. package/engine/commands/install.cjs +27 -0
  62. package/engine/commands/journal.cjs +31 -0
  63. package/engine/commands/legacy-network.cjs +29 -0
  64. package/engine/commands/list.cjs +49 -0
  65. package/engine/commands/login.cjs +68 -0
  66. package/engine/commands/logout.cjs +28 -0
  67. package/engine/commands/mcp.cjs +91 -0
  68. package/engine/commands/memory.cjs +21 -0
  69. package/engine/commands/multimodal.cjs +91 -0
  70. package/engine/commands/native.cjs +34 -0
  71. package/engine/commands/netadmin.cjs +31 -0
  72. package/engine/commands/oberon.cjs +70 -0
  73. package/engine/commands/ontology.cjs +24 -0
  74. package/engine/commands/open.cjs +48 -0
  75. package/engine/commands/plugin.cjs +101 -0
  76. package/engine/commands/project.cjs +47 -0
  77. package/engine/commands/research.cjs +36 -0
  78. package/engine/commands/route.cjs +37 -0
  79. package/engine/commands/run.cjs +149 -0
  80. package/engine/commands/search.cjs +55 -0
  81. package/engine/commands/setup.cjs +45 -0
  82. package/engine/commands/storm.cjs +75 -0
  83. package/engine/commands/swarm.cjs +75 -0
  84. package/engine/commands/telegram.cjs +32 -0
  85. package/engine/commands/uninstall.cjs +68 -0
  86. package/engine/commands/update.cjs +54 -0
  87. package/engine/commands/upload.cjs +18 -0
  88. package/engine/commands/usage.cjs +35 -0
  89. package/engine/commands/variant.cjs +25 -0
  90. package/engine/commands/version.cjs +9 -0
  91. package/engine/commands/whoami.cjs +38 -0
  92. package/engine/commands/workforce.cjs +103 -0
  93. package/engine/core/db.cjs +160 -0
  94. package/engine/core/paths.cjs +35 -0
  95. package/engine/experience/build.cjs +181 -0
  96. package/engine/experience/intents.cjs +492 -0
  97. package/engine/experience/runtime.cjs +242 -0
  98. package/engine/experience/variant.cjs +196 -0
  99. package/engine/firms/orchestrate.cjs +333 -0
  100. package/engine/hephaestus/runtime.cjs +697 -0
  101. package/engine/hub/install.cjs +872 -0
  102. package/engine/hub/plugins.cjs +213 -0
  103. package/engine/mcp/consent.cjs +289 -0
  104. package/engine/mcp/contract.cjs +202 -0
  105. package/engine/mcp/index.cjs +43 -0
  106. package/engine/mcp/inventory.cjs +322 -0
  107. package/engine/mcp/plan.cjs +286 -0
  108. package/engine/mcp/probe.cjs +151 -0
  109. package/engine/memory-cli/curate.cjs +163 -0
  110. package/engine/oberon/common.cjs +69 -0
  111. package/engine/oberon/manifest.cjs +164 -0
  112. package/engine/oberon/outputs.cjs +70 -0
  113. package/engine/oberon/render.cjs +164 -0
  114. package/engine/project/career-graph.cjs +249 -0
  115. package/engine/project/credentials.cjs +262 -0
  116. package/engine/project/env-file.cjs +46 -0
  117. package/engine/project/index.cjs +27 -0
  118. package/engine/project/memory-context.cjs +453 -0
  119. package/engine/project/ontology.cjs +467 -0
  120. package/engine/project/paths.cjs +39 -0
  121. package/engine/project/seed.cjs +200 -0
  122. package/engine/project/state.cjs +403 -0
  123. package/engine/project/super-ontology-seed.json +3288 -0
  124. package/engine/runtimes/detect.cjs +54 -0
  125. package/engine/runtimes/overrides.cjs +139 -0
  126. package/engine/runtimes/resolve.cjs +64 -0
  127. package/engine/sessions/apply-fences.cjs +188 -0
  128. package/engine/sessions/fences.cjs +362 -0
  129. package/engine/sessions/orchestrator.cjs +170 -0
  130. package/engine/sessions/prompt.cjs +212 -0
  131. package/engine/sessions/session.cjs +245 -0
  132. package/engine/sessions/sink.cjs +54 -0
  133. package/engine/sessions/store.cjs +79 -0
  134. package/engine/storm/deps.cjs +88 -0
  135. package/engine/storm/storm.cjs +218 -0
  136. package/engine/storm/swarm.cjs +422 -0
  137. package/engine/ui/palette.cjs +105 -0
  138. package/engine/ui/renderer.cjs +85 -0
  139. package/engine/ui/repl.cjs +444 -0
  140. package/engine/workforce/capture.cjs +701 -0
  141. package/engine/workforce/deps.cjs +472 -0
  142. package/package.json +2 -6
  143. package/engine/agentlas-experience-mcp.cjs +0 -1709
  144. package/engine/agentlas-parity.cjs +0 -1499
  145. package/engine/agentlas-repl.cjs +0 -1780
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ /*
3
+ * cloud-assets/restore — 소유자 Agent Cloud 자산의 나열/복원.
4
+ *
5
+ * 계약(약화 금지):
6
+ * - list는 서버 검색 결과의 리비전 디스크립터를 로컬 관측 상태로 승격한다.
7
+ * (다음 save/delete의 CAS 베이스는 반드시 "관측한" 리비전이어야 한다.)
8
+ * - restore 영수증(schema/source/owner/slug/해시/리비전)이 완전히 맞아야만
9
+ * 로컬 설치를 진행한다. 봉투와 cloudPackage가 어긋나면 invalid_restore_contract.
10
+ * - 실제 파일 물질화/DB 반영은 hub/install.cjs persistCloudListing을 재사용한다
11
+ * (동일 무결성·crash-recovery 계약, 복제 금지).
12
+ */
13
+ const {
14
+ CLOUD_PACKAGE_HASH_V1,
15
+ cloudPackageHashVersion,
16
+ normalizeCloudAssetDescriptor,
17
+ persistCloudListing,
18
+ } = require("../hub/install.cjs");
19
+ const { cargoListAgents, cargoRestorePackage } = require("./cargo.cjs");
20
+ const state = require("./state.cjs");
21
+
22
+ async function listOwnedCloudAgents(limit = 100) {
23
+ const result = (await cargoListAgents(limit)) || {
24
+ schema: "agentlas.agent_cloud.search.v1",
25
+ source: "cloud",
26
+ status: "ok",
27
+ count: 0,
28
+ total: 0,
29
+ results: [],
30
+ };
31
+ if (!Array.isArray(result.results)) throw new Error("Agent Cloud list returned an invalid results contract.");
32
+ if (result.results.length) {
33
+ const stateValue = state.readCloudAssetState();
34
+ for (const raw of result.results) {
35
+ const descriptor = normalizeCloudAssetDescriptor(raw, "Agent Cloud list result");
36
+ const key = state.cloudDescriptorKey(descriptor);
37
+ const previous = stateValue.assets[key];
38
+ const preserveRoots = previous && previous.descriptor.cloudId === descriptor.cloudId && previous.descriptor.revision === descriptor.revision;
39
+ stateValue.assets[key] = { descriptor, sourceRoots: preserveRoots ? previous.sourceRoots : [] };
40
+ }
41
+ state.writeCloudAssetState(stateValue);
42
+ }
43
+ return result;
44
+ }
45
+
46
+ function normalizeOwnerRestorePayload(raw, expectedSlug) {
47
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("invalid_restore_contract");
48
+ if (raw.schema !== "agentlas.agent_cloud.restore.v1" || raw.source !== "cloud" || raw.owner !== true) {
49
+ throw new Error("invalid_restore_contract");
50
+ }
51
+ if (typeof raw.slug !== "string" || !raw.slug || raw.slug !== expectedSlug) {
52
+ // cross-slug 응답을 그대로 설치하면 다른 자산이 요청 슬러그로 위장 설치된다.
53
+ throw new Error(`restore_slug_mismatch: requested ${expectedSlug}; received ${String(raw.slug || "")}`);
54
+ }
55
+ const pkg = raw.cloudPackage;
56
+ if (!pkg || typeof pkg !== "object" || Array.isArray(pkg) || !Array.isArray(pkg.files)) {
57
+ throw new Error("invalid_restore_contract");
58
+ }
59
+ const version = cloudPackageHashVersion(pkg.packageHashVersion);
60
+ if (!version || !/^[a-f0-9]{64}$/i.test(String(pkg.packageHash || "").replace(/^sha256:/i, ""))) {
61
+ throw new Error("invalid_restore_contract");
62
+ }
63
+ let descriptor;
64
+ let nestedDescriptor;
65
+ try {
66
+ descriptor = normalizeCloudAssetDescriptor(raw, "owner restore receipt");
67
+ nestedDescriptor = normalizeCloudAssetDescriptor({
68
+ ...pkg,
69
+ slug: raw.slug,
70
+ etag: raw.etag,
71
+ }, "owner restore package receipt");
72
+ } catch (error) {
73
+ throw new Error(`invalid_restore_contract: ${error.message || error}`);
74
+ }
75
+ if (JSON.stringify(descriptor) !== JSON.stringify(nestedDescriptor)) {
76
+ throw new Error("invalid_restore_contract: restore revision envelope and cloudPackage disagree");
77
+ }
78
+ if (!["agent", "team", "repo"].includes(pkg.agentKind) || !Number.isSafeInteger(pkg.fileCount) || !Number.isSafeInteger(pkg.totalBytes)) {
79
+ throw new Error("invalid_restore_contract");
80
+ }
81
+ for (const file of pkg.files) {
82
+ if (!file || typeof file !== "object" || typeof file.path !== "string" || !Number.isSafeInteger(file.bytes) || typeof file.sha256 !== "string" || typeof file.contentBase64 !== "string") {
83
+ throw new Error("invalid_restore_contract");
84
+ }
85
+ }
86
+ const outerVersion = raw.packageHashVersion == null ? version : cloudPackageHashVersion(raw.packageHashVersion);
87
+ if (
88
+ (raw.packageHash != null && String(raw.packageHash) !== String(pkg.packageHash)) ||
89
+ !outerVersion || outerVersion !== version ||
90
+ (raw.fileCount != null && raw.fileCount !== pkg.fileCount) ||
91
+ (raw.totalBytes != null && raw.totalBytes !== pkg.totalBytes) ||
92
+ (raw.agentKind != null && raw.agentKind !== pkg.agentKind)
93
+ ) {
94
+ throw new Error("invalid_restore_contract: restore envelope and cloudPackage disagree");
95
+ }
96
+ return {
97
+ schema: raw.schema,
98
+ source: raw.source,
99
+ owner: true,
100
+ slug: raw.slug,
101
+ name: typeof raw.name === "string" && raw.name ? raw.name : raw.slug,
102
+ nameEn: typeof raw.nameEn === "string" && raw.nameEn ? raw.nameEn : (raw.name || raw.slug),
103
+ tagline: typeof raw.tagline === "string" ? raw.tagline : "",
104
+ taglineEn: typeof raw.taglineEn === "string" ? raw.taglineEn : (raw.tagline || ""),
105
+ descriptor,
106
+ cloudPackage: {
107
+ cloudId: descriptor.cloudId,
108
+ scope: descriptor.scope,
109
+ revision: descriptor.revision,
110
+ updatedAt: descriptor.updatedAt,
111
+ packageHash: String(pkg.packageHash).replace(/^sha256:/i, "").toLowerCase(),
112
+ packageHashVersion: version,
113
+ fileCount: pkg.fileCount,
114
+ totalBytes: pkg.totalBytes,
115
+ agentKind: pkg.agentKind,
116
+ runtimeLabels: Array.isArray(pkg.runtimeLabels) ? pkg.runtimeLabels.filter((item) => typeof item === "string" && item.trim()) : [],
117
+ files: pkg.files,
118
+ },
119
+ };
120
+ }
121
+
122
+ async function restoreOwnedCloudAgent(db, slug) {
123
+ const raw = await cargoRestorePackage(slug);
124
+ if (!raw || raw.error) {
125
+ const code = raw && raw.error ? raw.error : "agent_not_found";
126
+ const message = raw && raw.message ? raw.message : `Agent Cloud package not found: ${slug}`;
127
+ throw new Error(`${code}: ${message}`);
128
+ }
129
+ const restored = normalizeOwnerRestorePayload(raw, slug);
130
+ const cloudPackage = restored.cloudPackage;
131
+ const listing = {
132
+ slug: restored.slug || slug,
133
+ name: restored.name || restored.nameEn || restored.slug || slug,
134
+ nameEn: restored.nameEn || restored.name || restored.slug || slug,
135
+ tagline: restored.tagline || restored.taglineEn || "",
136
+ taglineEn: restored.taglineEn || restored.tagline || "",
137
+ trustGrade: "A",
138
+ visibility: "visible",
139
+ source: "cloud",
140
+ assetDescriptor: restored.descriptor,
141
+ cloudPackage,
142
+ };
143
+ const agent = persistCloudListing(db, listing);
144
+ let descriptor = restored.descriptor;
145
+ let localStateWarning;
146
+ try {
147
+ descriptor = state.rememberCloudAssetDescriptor(restored.descriptor, { sourceRoot: agent.localPath || undefined });
148
+ } catch (error) {
149
+ localStateWarning = `Restore completed, but observed revision state could not be indexed: ${error.message || error}`;
150
+ }
151
+ return {
152
+ schema: restored.schema || "agentlas.agent_cloud.restore.v1",
153
+ source: "cloud",
154
+ slug: agent.slug,
155
+ name: agent.name,
156
+ packageHash: cloudPackage.packageHash,
157
+ packageHashVersion: cloudPackage.packageHashVersion || CLOUD_PACKAGE_HASH_V1,
158
+ cloudId: descriptor.cloudId,
159
+ scope: descriptor.scope,
160
+ revision: descriptor.revision,
161
+ etag: descriptor.etag,
162
+ updatedAt: descriptor.updatedAt,
163
+ localPath: agent.localPath || null,
164
+ ...(localStateWarning ? { localStateWarning } : {}),
165
+ };
166
+ }
167
+
168
+ module.exports = {
169
+ listOwnedCloudAgents,
170
+ normalizeOwnerRestorePayload,
171
+ restoreOwnedCloudAgent,
172
+ };
@@ -0,0 +1,268 @@
1
+ "use strict";
2
+ /*
3
+ * cloud-assets/state — Agent Cloud 자산 리비전의 로컬 관측 상태.
4
+ *
5
+ * 두 저장소를 관리한다 (v1 monolith의 asset-state 계열 충실 이식):
6
+ * 1. <userData>/cloud-asset-state.v1.json — 이 머신이 "관측한" 각 자산의
7
+ * 마지막 리비전 디스크립터 + 소스 루트 목록 + 삭제 톰스톤(deletedBases).
8
+ * 2. <sourceRoot>/.agentlas-cloud-package.json — 소스 폴더 마커. scope별
9
+ * 디스크립터(cloudAssets)를 담아 다음 save의 CAS 베이스가 된다.
10
+ *
11
+ * 계약(약화 금지):
12
+ * - 관측한 베이스 리비전 없이는 어떤 덮어쓰기도 시도하지 않는다. 이 파일이
13
+ * 깨졌으면 조용히 새로 만들지 않고 정직하게 실패한다 (조용한 기본값 금지).
14
+ * - 삭제 톰스톤: delete가 커밋된 (rootPath, slug, scope, cloudId, revision)은
15
+ * 다시 베이스로 채택되지 않는다 — delete→재저장은 반드시 새 생성(If-None-Match)이다.
16
+ * - 상태 파일 쓰기는 O_EXCL temp → fsync → rename → dir fsync (크래시 원자성).
17
+ */
18
+ const fs = require("node:fs");
19
+ const path = require("node:path");
20
+ const crypto = require("node:crypto");
21
+ const { userDataDir } = require("../core/paths.cjs");
22
+ const {
23
+ CLOUD_ASSET_SCOPES,
24
+ CLOUD_PACKAGE_HASH_V1,
25
+ CLOUD_RESTORE_MARKER_PATH,
26
+ cloudSlug,
27
+ cloudApplyPortableFileMode,
28
+ cloudFsyncDirectory,
29
+ normalizeCloudAssetDescriptor,
30
+ } = require("../hub/install.cjs");
31
+
32
+ const CLOUD_ASSET_STATE_FILE = "cloud-asset-state.v1.json";
33
+
34
+ function normalizeCloudScopeFlag(value) {
35
+ if (value === "owner-private" || value === "private" || value === "private-link") return "owner-private";
36
+ if (value === "hub-public" || value === "marketplace" || value === "public") return "hub-public";
37
+ return null;
38
+ }
39
+
40
+ function cloudDescriptorKey(descriptor) {
41
+ return `${descriptor.scope}:${descriptor.slug}`;
42
+ }
43
+
44
+ function cloudAssetStatePath() {
45
+ return path.join(userDataDir(), CLOUD_ASSET_STATE_FILE);
46
+ }
47
+
48
+ function readCloudAssetState() {
49
+ const statePath = cloudAssetStatePath();
50
+ if (!fs.existsSync(statePath)) return { schemaVersion: 1, assets: {}, deletedBases: [] };
51
+ let fd;
52
+ try {
53
+ // O_NOFOLLOW: 상태 파일이 심링크로 바꿔치기되면 읽지 않는다 (로컬 상태 위조 방어).
54
+ fd = fs.openSync(statePath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
55
+ const stat = fs.fstatSync(fd);
56
+ if (!stat.isFile() || stat.size > 1024 * 1024) throw new Error("state file is not a bounded regular file");
57
+ const parsed = JSON.parse(fs.readFileSync(fd, "utf8"));
58
+ if (!parsed || parsed.schemaVersion !== 1 || !parsed.assets || typeof parsed.assets !== "object" || Array.isArray(parsed.assets)) {
59
+ throw new Error("state schema is invalid");
60
+ }
61
+ const assets = {};
62
+ for (const [key, raw] of Object.entries(parsed.assets)) {
63
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`state entry ${key} is invalid`);
64
+ const descriptor = normalizeCloudAssetDescriptor(raw.descriptor, `state entry ${key}`);
65
+ if (key !== cloudDescriptorKey(descriptor)) throw new Error(`state entry ${key} key is invalid`);
66
+ const sourceRoots = Array.isArray(raw.sourceRoots)
67
+ ? [...new Set(raw.sourceRoots.filter((item) => typeof item === "string" && path.isAbsolute(item)).map((item) => path.resolve(item)))].slice(0, 32)
68
+ : [];
69
+ assets[key] = { descriptor, sourceRoots };
70
+ }
71
+ const deletedBases = Array.isArray(parsed.deletedBases)
72
+ ? parsed.deletedBases.filter((item) =>
73
+ item && typeof item === "object" && !Array.isArray(item) &&
74
+ typeof item.rootPath === "string" && path.isAbsolute(item.rootPath) &&
75
+ typeof item.slug === "string" && cloudSlug(item.slug) === item.slug &&
76
+ CLOUD_ASSET_SCOPES.has(item.scope) && typeof item.cloudId === "string" &&
77
+ typeof item.revision === "string"
78
+ ).map((item) => ({
79
+ rootPath: path.resolve(item.rootPath),
80
+ slug: item.slug,
81
+ scope: item.scope,
82
+ cloudId: item.cloudId,
83
+ revision: item.revision,
84
+ })).slice(-256)
85
+ : [];
86
+ return { schemaVersion: 1, assets, deletedBases };
87
+ } catch (error) {
88
+ // 깨진 상태 파일을 빈 상태로 위장하면 stale 베이스로 원격 리비전을 덮어쓸 수 있다.
89
+ throw new Error(`Agent Cloud local revision state is unreadable: ${error.message || error}`);
90
+ } finally {
91
+ if (fd !== undefined) try { fs.closeSync(fd); } catch { /* best-effort */ }
92
+ }
93
+ }
94
+
95
+ function writeCloudAssetState(state) {
96
+ const statePath = cloudAssetStatePath();
97
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
98
+ const temp = `${statePath}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
99
+ const fd = fs.openSync(temp, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
100
+ try {
101
+ fs.writeFileSync(fd, JSON.stringify(state, null, 2) + "\n", "utf8");
102
+ fs.fsyncSync(fd);
103
+ } finally {
104
+ fs.closeSync(fd);
105
+ }
106
+ fs.renameSync(temp, statePath);
107
+ cloudApplyPortableFileMode(statePath, 0o600);
108
+ cloudFsyncDirectory(path.dirname(statePath));
109
+ }
110
+
111
+ /** 서버가 준 리비전 영수증을 관측 상태로 승격. sourceRoot가 있으면 톰스톤도 해제한다. */
112
+ function rememberCloudAssetDescriptor(value, options = {}) {
113
+ const descriptor = normalizeCloudAssetDescriptor(value);
114
+ const state = readCloudAssetState();
115
+ const key = cloudDescriptorKey(descriptor);
116
+ const previous = state.assets[key];
117
+ const sameRevision = previous && previous.descriptor.cloudId === descriptor.cloudId && previous.descriptor.revision === descriptor.revision;
118
+ const roots = sameRevision ? [...previous.sourceRoots] : [];
119
+ if (options.sourceRoot) {
120
+ const sourceRoot = path.resolve(options.sourceRoot);
121
+ roots.push(sourceRoot);
122
+ state.deletedBases = state.deletedBases.filter(
123
+ (item) => !(item.rootPath === sourceRoot && item.slug === descriptor.slug && item.scope === descriptor.scope),
124
+ );
125
+ }
126
+ state.assets[key] = { descriptor, sourceRoots: [...new Set(roots)].slice(0, 32) };
127
+ writeCloudAssetState(state);
128
+ return descriptor;
129
+ }
130
+
131
+ function findCloudAssetDescriptor(slug, scope) {
132
+ const safeSlug = cloudSlug(slug);
133
+ const state = readCloudAssetState();
134
+ const matches = Object.values(state.assets).filter(
135
+ (entry) => entry.descriptor.slug === safeSlug && (!scope || entry.descriptor.scope === scope),
136
+ );
137
+ if (!scope && matches.length > 1) {
138
+ // 같은 슬러그가 두 scope에 있으면 "아무거나" 지우지 않는다 — 정확한 지정 강제.
139
+ throw new Error(`Cloud asset ${safeSlug} exists in multiple scopes. Retry with --scope owner-private or --scope hub-public.`);
140
+ }
141
+ return matches.length === 1 ? matches[0] : null;
142
+ }
143
+
144
+ // ── 소스 폴더 마커 (.agentlas-cloud-package.json) ──
145
+
146
+ function cloudMarkerDescriptors(marker) {
147
+ const descriptors = {};
148
+ if (!marker || typeof marker !== "object" || Array.isArray(marker)) return descriptors;
149
+ if (marker.cloudAssets && typeof marker.cloudAssets === "object" && !Array.isArray(marker.cloudAssets)) {
150
+ for (const scope of CLOUD_ASSET_SCOPES) {
151
+ if (!marker.cloudAssets[scope]) continue;
152
+ try {
153
+ const descriptor = normalizeCloudAssetDescriptor(marker.cloudAssets[scope], `local marker ${scope}`);
154
+ if (descriptor.scope === scope) descriptors[scope] = descriptor;
155
+ } catch { /* 레거시/손상 CAS 항목은 베이스 리비전으로 채택하지 않는다 */ }
156
+ }
157
+ }
158
+ if (marker.revision && marker.cloudId && marker.scope) {
159
+ try {
160
+ const descriptor = normalizeCloudAssetDescriptor(marker, "local marker");
161
+ if (!descriptors[descriptor.scope]) descriptors[descriptor.scope] = descriptor;
162
+ } catch { /* legacy marker */ }
163
+ }
164
+ return descriptors;
165
+ }
166
+
167
+ function cloudBaseDescriptorFromMarker(marker, slug, scope) {
168
+ const descriptor = cloudMarkerDescriptors(marker)[scope];
169
+ return descriptor && descriptor.slug === slug ? descriptor : null;
170
+ }
171
+
172
+ /**
173
+ * 다음 save의 CAS 베이스 결정: 마커 vs 상태 저널.
174
+ * 톰스톤에 걸린 마커 디스크립터는 무시(삭제 후 재저장은 새 생성이어야 한다).
175
+ * 둘 다 있으면 같은 cloudId에서 더 최신 관측을 채택한다.
176
+ */
177
+ function cloudBaseDescriptorForSource(marker, rootPath, slug, scope) {
178
+ const state = readCloudAssetState();
179
+ const normalizedRoot = path.resolve(rootPath);
180
+ let markerDescriptor = cloudBaseDescriptorFromMarker(marker, slug, scope);
181
+ if (markerDescriptor && state.deletedBases.some((item) =>
182
+ item.rootPath === normalizedRoot && item.slug === slug && item.scope === scope &&
183
+ item.cloudId === markerDescriptor.cloudId && item.revision === markerDescriptor.revision
184
+ )) {
185
+ markerDescriptor = null;
186
+ }
187
+ const entry = state.assets[`${scope}:${slug}`];
188
+ const stateDescriptor = entry && entry.sourceRoots.includes(normalizedRoot) ? entry.descriptor : null;
189
+ if (!markerDescriptor) return stateDescriptor;
190
+ if (!stateDescriptor) return markerDescriptor;
191
+ return stateDescriptor.cloudId === markerDescriptor.cloudId && stateDescriptor.updatedAt >= markerDescriptor.updatedAt
192
+ ? stateDescriptor
193
+ : markerDescriptor;
194
+ }
195
+
196
+ function writeCloudSourceMarker(rootPath, scan, descriptor, options = {}) {
197
+ const markerPath = path.join(rootPath, CLOUD_RESTORE_MARKER_PATH);
198
+ if (fs.existsSync(markerPath)) {
199
+ const stat = fs.lstatSync(markerPath);
200
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("Agent Cloud revision marker is not a regular file");
201
+ }
202
+ const descriptors = cloudMarkerDescriptors(options.previousMarker);
203
+ if (descriptor) descriptors[descriptor.scope] = descriptor;
204
+ if (options.removeDescriptor) {
205
+ const current = descriptors[options.removeDescriptor.scope];
206
+ if (current && current.cloudId === options.removeDescriptor.cloudId && current.revision === options.removeDescriptor.revision) {
207
+ delete descriptors[options.removeDescriptor.scope];
208
+ }
209
+ }
210
+ const latest = descriptor || Object.values(descriptors)[0] || null;
211
+ const marker = {
212
+ schemaVersion: 1,
213
+ source: "agentlas-cloud",
214
+ slug: latest?.slug || options.removeDescriptor?.slug || cloudSlug(path.basename(rootPath)),
215
+ packageHash: descriptor?.packageHash || options.packageHash || options.previousMarker?.packageHash || "",
216
+ packageHashVersion: descriptor?.packageHashVersion || options.packageHashVersion || options.previousMarker?.packageHashVersion || CLOUD_PACKAGE_HASH_V1,
217
+ fileCount: Number.isSafeInteger(options.fileCount) ? options.fileCount : (options.previousMarker?.fileCount || 0),
218
+ totalBytes: Number.isSafeInteger(options.totalBytes) ? options.totalBytes : (options.previousMarker?.totalBytes || 0),
219
+ executablePaths: Array.isArray(options.executablePaths) ? options.executablePaths : options.previousMarker?.executablePaths,
220
+ cloudAssets: descriptors,
221
+ ...(latest ? latest : {}),
222
+ restoredAt: options.previousMarker?.restoredAt,
223
+ savedAt: new Date().toISOString(),
224
+ };
225
+ for (const key of Object.keys(marker)) if (marker[key] === undefined) delete marker[key];
226
+ const temp = path.join(rootPath, `.${CLOUD_RESTORE_MARKER_PATH}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`);
227
+ const fd = fs.openSync(temp, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
228
+ try {
229
+ fs.writeFileSync(fd, JSON.stringify(marker, null, 2) + "\n", "utf8");
230
+ fs.fsyncSync(fd);
231
+ } finally {
232
+ fs.closeSync(fd);
233
+ }
234
+ fs.renameSync(temp, markerPath);
235
+ cloudApplyPortableFileMode(markerPath, 0o600);
236
+ cloudFsyncDirectory(rootPath);
237
+ return marker;
238
+ }
239
+
240
+ function readCloudSourceMarker(rootPath) {
241
+ const markerPath = path.join(rootPath, CLOUD_RESTORE_MARKER_PATH);
242
+ if (!fs.existsSync(markerPath)) return null;
243
+ let fd;
244
+ try {
245
+ fd = fs.openSync(markerPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
246
+ const stat = fs.fstatSync(fd);
247
+ if (!stat.isFile() || stat.size > 1024 * 1024) throw new Error("marker is not a bounded regular file");
248
+ return JSON.parse(fs.readFileSync(fd, "utf8"));
249
+ } finally {
250
+ if (fd !== undefined) fs.closeSync(fd);
251
+ }
252
+ }
253
+
254
+ module.exports = {
255
+ CLOUD_ASSET_STATE_FILE,
256
+ normalizeCloudScopeFlag,
257
+ cloudDescriptorKey,
258
+ cloudAssetStatePath,
259
+ readCloudAssetState,
260
+ writeCloudAssetState,
261
+ rememberCloudAssetDescriptor,
262
+ findCloudAssetDescriptor,
263
+ cloudMarkerDescriptors,
264
+ cloudBaseDescriptorFromMarker,
265
+ cloudBaseDescriptorForSource,
266
+ writeCloudSourceMarker,
267
+ readCloudSourceMarker,
268
+ };
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+ /*
3
+ * automation — 자동화 등록/목록/토글/삭제/실행 (v1 cmdAutomation 의 v2 포팅).
4
+ * list (기본) | add | on <id> | off <id> | remove <id> | run <id> | runs | daemon
5
+ *
6
+ * 스케줄 계산은 automation/schedule, DB는 automation/store, 실행은
7
+ * automation/daemon(세션 계층)만 쓴다. run <id> 는 스케줄을 건드리지 않는다
8
+ * (앱의 advanceSchedule=false 와 동일); 예약 전진은 daemon 경로에서만 일어난다.
9
+ */
10
+ const { findAgent } = require("../agents/registry.cjs");
11
+ const schedule = require("../automation/schedule.cjs");
12
+ const store = require("../automation/store.cjs");
13
+ const daemon = require("../automation/daemon.cjs");
14
+
15
+ function resolveFirm(db, token) {
16
+ const q = String(token || "").trim();
17
+ if (!q) return null;
18
+ try {
19
+ return db.prepare("SELECT * FROM firms WHERE id = ? OR lower(slug) = lower(?) OR name = ?").get(q, q, q) || null;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ async function run(ctx, args) {
26
+ const sub = args[0] || "list";
27
+ const ko = ctx.lang === "ko";
28
+ const db = ctx.db();
29
+ const usage = (tail) => (ko ? `사용법: agentlas automation ${tail}` : `usage: agentlas automation ${tail}`);
30
+ const missingAutomation = (id) => (ko ? `자동화를 찾지 못했습니다: ${id}` : `Automation not found: ${id}`);
31
+ const fail = (msg) => { ctx.err(msg); return 1; };
32
+
33
+ if (sub === "list") {
34
+ const rows = store.listAutomations(db);
35
+ if (!rows.length) {
36
+ ctx.out(ko
37
+ ? "자동화가 없습니다. `agentlas automation add --help`로 추가 방법을 확인하세요."
38
+ : "No automations. Run agentlas automation add --help.");
39
+ return 0;
40
+ }
41
+ for (const r of rows) {
42
+ const target = r.target_type + ":" + String(r.target_id).slice(0, 24);
43
+ ctx.out(
44
+ `${r.enabled ? ctx.ui.green("●") : ctx.ui.dim("○")} ${ctx.ui.bold(String(r.id).slice(0, 8))} ${String(r.name).padEnd(28).slice(0, 28)} ` +
45
+ `${String(r.schedule || r.trigger_type || "-").padEnd(14)} ${target.padEnd(32)} ` +
46
+ `${ko ? "다음" : "next"}=${r.next_run_at ? r.next_run_at.slice(0, 16) : "-"} ` +
47
+ `${ko ? "성공" : "success"}=${r.run_count ?? 0}`,
48
+ );
49
+ }
50
+ ctx.out("");
51
+ ctx.out(ctx.ui.dim(ko
52
+ ? "지금 실행: agentlas automation run <id> · 데몬: agentlas automation daemon"
53
+ : "Run now: agentlas automation run <id> · daemon: agentlas automation daemon"));
54
+ ctx.out(ctx.ui.dim(ko
55
+ ? "Desktop 앱이 열려 있을 때는 Desktop 스케줄러도 실행되며, 리스로 중복 실행을 막습니다."
56
+ : "The Desktop scheduler also runs when the app is open; leases prevent duplicate runs."));
57
+ return 0;
58
+ }
59
+
60
+ if (sub === "add") {
61
+ // agentlas automation add --name "..." --agent <slug>|--firm <slug> --cron "0 9 * * *" --prompt "..."
62
+ const flags = {};
63
+ for (let i = 1; i < args.length; i++) {
64
+ const a = args[i];
65
+ if (a === "--name") flags.name = args[++i];
66
+ else if (a === "--agent") flags.agent = args[++i];
67
+ else if (a === "--firm") flags.firm = args[++i];
68
+ else if (a === "--cron") flags.cron = args[++i];
69
+ else if (a === "--prompt") flags.prompt = args[++i];
70
+ else if (a === "--tz") flags.tz = args[++i];
71
+ else if (a === "--disabled") flags.disabled = true;
72
+ }
73
+ if (!flags.cron || !flags.prompt || (!flags.agent && !flags.firm)) {
74
+ ctx.err(ko
75
+ ? '사용법: agentlas automation add --name "이름" --agent <슬러그>|--firm <슬러그> --cron "0 9 * * *" --prompt "지시" [--tz Asia/Seoul] [--disabled]'
76
+ : 'usage: agentlas automation add --name "name" --agent <slug>|--firm <slug> --cron "0 9 * * *" --prompt "instructions" [--tz Asia/Seoul] [--disabled]');
77
+ return 1;
78
+ }
79
+ let targetType;
80
+ let targetId;
81
+ let targetLabel;
82
+ if (flags.agent) {
83
+ const a = findAgent(db, flags.agent);
84
+ if (!a) return fail(ko ? `에이전트를 찾지 못했습니다: ${flags.agent}` : `Agent not found: ${flags.agent}`);
85
+ targetType = "agent";
86
+ targetId = a.id;
87
+ targetLabel = a.name;
88
+ } else {
89
+ const f = resolveFirm(db, flags.firm);
90
+ if (!f) return fail(ko ? `회사를 찾지 못했습니다: ${flags.firm}` : `Company not found: ${flags.firm}`);
91
+ targetType = "firm";
92
+ targetId = f.id;
93
+ targetLabel = f.name;
94
+ }
95
+ const next = schedule.nextCronRun(flags.cron, new Date(), flags.tz || null);
96
+ if (!next) {
97
+ return fail(ko
98
+ ? `cron 표현식을 해석하지 못했습니다: "${flags.cron}" (5개 필드: 분 시 일 월 요일)`
99
+ : `Could not parse cron expression: "${flags.cron}" (5 fields: minute hour day month weekday)`);
100
+ }
101
+ const id = store.addAutomation(db, {
102
+ name: flags.name || `${targetLabel} automation`,
103
+ targetType,
104
+ targetId,
105
+ cron: flags.cron,
106
+ prompt: flags.prompt,
107
+ tz: flags.tz || null,
108
+ disabled: !!flags.disabled,
109
+ }, next);
110
+ ctx.out(`${ko ? "생성됨" : "Created"}: ${ctx.ui.bold(id.slice(0, 8))} ${flags.name || targetLabel} ${ko ? "다음" : "next"}=${next.toISOString().slice(0, 16)}`);
111
+ ctx.out(ctx.ui.dim(ko
112
+ ? `지금 실행: agentlas automation run ${id.slice(0, 8)} · 예약 실행: agentlas automation daemon (또는 Desktop)`
113
+ : `Run now: agentlas automation run ${id.slice(0, 8)} · scheduled: agentlas automation daemon (or Desktop)`));
114
+ return 0;
115
+ }
116
+
117
+ if (sub === "on" || sub === "off") {
118
+ const idPrefix = args[1];
119
+ if (!idPrefix) return fail(usage(`${sub} <id>`));
120
+ const row = store.getAutomationByPrefix(db, idPrefix);
121
+ if (!row) return fail(missingAutomation(idPrefix));
122
+ store.setEnabled(db, row, sub === "on");
123
+ ctx.out(`${sub === "on" ? (ko ? "활성화됨" : "Enabled") : (ko ? "비활성화됨" : "Disabled")}: ${row.id.slice(0, 8)} ${row.name}`);
124
+ return 0;
125
+ }
126
+
127
+ if (sub === "remove" || sub === "rm") {
128
+ const idPrefix = args[1];
129
+ if (!idPrefix) return fail(usage("remove <id>"));
130
+ const row = store.getAutomationByPrefix(db, idPrefix);
131
+ if (!row) return fail(missingAutomation(idPrefix));
132
+ store.removeAutomation(db, row.id);
133
+ ctx.out(`${ko ? "삭제됨" : "Deleted"}: ${row.id.slice(0, 8)} ${row.name}`);
134
+ return 0;
135
+ }
136
+
137
+ if (sub === "runs") {
138
+ const rows = store.listRuns(db, 15);
139
+ if (!rows.length) {
140
+ ctx.out(ko ? "실행 기록이 없습니다." : "No run history.");
141
+ return 0;
142
+ }
143
+ for (const r of rows) {
144
+ const status = ko
145
+ ? (r.status === "ok" ? "성공" : r.status === "error" ? "오류" : (r.status || "?"))
146
+ : (r.status || "?");
147
+ ctx.out(
148
+ `${(r.ran_at || "").slice(0, 16).padEnd(17)} ${(r.status === "ok" ? ctx.ui.green(status.padEnd(9)) : ctx.ui.red(status.padEnd(9)))} ` +
149
+ `${(r.name || (ko ? "(삭제됨)" : "(deleted)")).slice(0, 30).padEnd(31)} ${r.error ? ctx.ui.dim(String(r.error).slice(0, 40)) : ""}`,
150
+ );
151
+ }
152
+ return 0;
153
+ }
154
+
155
+ if (sub === "run") {
156
+ const idPrefix = args[1];
157
+ let runtimeOverride = null;
158
+ for (let i = 2; i < args.length; i++) {
159
+ if (args[i] === "--runtime") runtimeOverride = args[++i];
160
+ }
161
+ if (!idPrefix) return fail(usage("run <id>"));
162
+ const row = store.getAutomationByPrefix(db, idPrefix);
163
+ if (!row) return fail(missingAutomation(idPrefix));
164
+ // run-now는 스케줄을 건드리지 않는다 (앱의 advanceSchedule=false와 동일).
165
+ let renderer = null;
166
+ const r = await daemon.runAutomationOnce(ctx, db, row, {
167
+ advanceSchedule: false,
168
+ runtimeOverride,
169
+ onSession: (session) => {
170
+ if (ctx.uiInstance) {
171
+ const { Renderer } = require("../ui/renderer.cjs");
172
+ renderer = new Renderer(ctx.uiInstance);
173
+ renderer.attach(session, { replay: false });
174
+ }
175
+ },
176
+ });
177
+ if (renderer) renderer.detach();
178
+ return r.ok ? 0 : 1;
179
+ }
180
+
181
+ if (sub === "daemon") {
182
+ let interval = 30;
183
+ let runtimeOverride = null;
184
+ for (let i = 1; i < args.length; i++) {
185
+ if (args[i] === "--interval") interval = Math.max(10, Number(args[++i]) || 30);
186
+ else if (args[i] === "--runtime") runtimeOverride = args[++i];
187
+ }
188
+ return daemon.automationDaemon(ctx, db, { intervalSec: interval, runtimeOverride });
189
+ }
190
+
191
+ ctx.err(usage("list|add|on <id>|off <id>|remove <id>|run <id>|runs|daemon"));
192
+ return 1;
193
+ }
194
+
195
+ module.exports = { run };