spawnfile 0.1.1 → 0.1.3

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 (102) hide show
  1. package/README.md +80 -396
  2. package/dist/cli/index.js +0 -0
  3. package/dist/cli/modelCommands.d.ts +3 -0
  4. package/dist/cli/modelCommands.js +68 -0
  5. package/dist/cli/runCli.d.ts +23 -2
  6. package/dist/cli/runCli.js +78 -122
  7. package/dist/cli/runtimeCommands.d.ts +3 -0
  8. package/dist/cli/runtimeCommands.js +20 -0
  9. package/dist/cli/surfaceCommands.d.ts +3 -0
  10. package/dist/cli/surfaceCommands.js +98 -0
  11. package/dist/cli/viewCommand.d.ts +3 -0
  12. package/dist/cli/viewCommand.js +87 -0
  13. package/dist/compiler/agentSurfaces.js +51 -5
  14. package/dist/compiler/buildCompilePlan.js +38 -40
  15. package/dist/compiler/buildCompilePlanRuntime.d.ts +14 -0
  16. package/dist/compiler/buildCompilePlanRuntime.js +39 -0
  17. package/dist/compiler/buildCompilePlanTeams.d.ts +5 -0
  18. package/dist/compiler/buildCompilePlanTeams.js +38 -0
  19. package/dist/compiler/compilePlanHelpers.js +4 -1
  20. package/dist/compiler/compileProject.js +62 -13
  21. package/dist/compiler/compileProjectSupport.d.ts +17 -0
  22. package/dist/compiler/compileProjectSupport.js +136 -0
  23. package/dist/compiler/containerArtifacts.d.ts +6 -1
  24. package/dist/compiler/containerArtifacts.js +26 -4
  25. package/dist/compiler/containerArtifactsPlans.js +16 -1
  26. package/dist/compiler/containerArtifactsRender.d.ts +4 -2
  27. package/dist/compiler/containerArtifactsRender.js +21 -126
  28. package/dist/compiler/containerArtifactsTypes.d.ts +7 -0
  29. package/dist/compiler/containerEntrypointRender.d.ts +12 -0
  30. package/dist/compiler/containerEntrypointRender.js +186 -0
  31. package/dist/compiler/index.d.ts +4 -0
  32. package/dist/compiler/index.js +4 -0
  33. package/dist/compiler/interactiveSurfaceScopes.d.ts +2 -0
  34. package/dist/compiler/interactiveSurfaceScopes.js +21 -0
  35. package/dist/compiler/moltnetArtifacts.d.ts +27 -0
  36. package/dist/compiler/moltnetArtifacts.js +208 -0
  37. package/dist/compiler/moltnetBinaries.d.ts +4 -0
  38. package/dist/compiler/moltnetBinaries.js +103 -0
  39. package/dist/compiler/moltnetClientConfig.d.ts +11 -0
  40. package/dist/compiler/moltnetClientConfig.js +89 -0
  41. package/dist/compiler/moltnetRepresentativeResolution.d.ts +16 -0
  42. package/dist/compiler/moltnetRepresentativeResolution.js +86 -0
  43. package/dist/compiler/moltnetResolution.d.ts +3 -0
  44. package/dist/compiler/moltnetResolution.js +182 -0
  45. package/dist/compiler/moltnetRoomMemberships.d.ts +3 -0
  46. package/dist/compiler/moltnetRoomMemberships.js +140 -0
  47. package/dist/compiler/runProject.js +1 -1
  48. package/dist/compiler/surfaceDefinitions.d.ts +55 -0
  49. package/dist/compiler/surfaceDefinitions.js +204 -0
  50. package/dist/compiler/teamContextHelpers.d.ts +18 -0
  51. package/dist/compiler/teamContextHelpers.js +112 -0
  52. package/dist/compiler/teamContextSupport.d.ts +4 -0
  53. package/dist/compiler/teamContextSupport.js +264 -0
  54. package/dist/compiler/teamContextSupport.testHelpers.d.ts +16 -0
  55. package/dist/compiler/teamContextSupport.testHelpers.js +68 -0
  56. package/dist/compiler/teamContextTypes.d.ts +28 -0
  57. package/dist/compiler/teamContextTypes.js +1 -0
  58. package/dist/compiler/teamRoster.d.ts +12 -0
  59. package/dist/compiler/teamRoster.js +48 -0
  60. package/dist/compiler/teamRosterEntries.d.ts +13 -0
  61. package/dist/compiler/teamRosterEntries.js +230 -0
  62. package/dist/compiler/teamRosterTypes.d.ts +45 -0
  63. package/dist/compiler/teamRosterTypes.js +1 -0
  64. package/dist/compiler/types.d.ts +90 -6
  65. package/dist/compiler/updateProjectRuntime.d.ts +9 -0
  66. package/dist/compiler/updateProjectRuntime.js +67 -0
  67. package/dist/compiler/updateProjectSurfaces.d.ts +8 -0
  68. package/dist/compiler/updateProjectSurfaces.js +106 -0
  69. package/dist/compiler/view/buildOrganizationView.d.ts +2 -0
  70. package/dist/compiler/view/buildOrganizationView.js +180 -0
  71. package/dist/compiler/view/index.d.ts +4 -0
  72. package/dist/compiler/view/index.js +4 -0
  73. package/dist/compiler/view/renderNetworks.d.ts +2 -0
  74. package/dist/compiler/view/renderNetworks.js +93 -0
  75. package/dist/compiler/view/renderTree.d.ts +2 -0
  76. package/dist/compiler/view/renderTree.js +59 -0
  77. package/dist/compiler/view/sourcePaths.d.ts +2 -0
  78. package/dist/compiler/view/sourcePaths.js +19 -0
  79. package/dist/compiler/view/types.d.ts +80 -0
  80. package/dist/compiler/view/types.js +1 -0
  81. package/dist/manifest/loadManifest.js +4 -4
  82. package/dist/manifest/renderSpawnfile.js +74 -8
  83. package/dist/manifest/scaffold.js +1 -3
  84. package/dist/manifest/schemas.d.ts +227 -17
  85. package/dist/manifest/schemas.js +62 -20
  86. package/dist/manifest/surfaceSchemas.d.ts +154 -0
  87. package/dist/manifest/surfaceSchemas.js +77 -5
  88. package/dist/runtime/common.js +3 -0
  89. package/dist/runtime/openclaw/adapter.js +38 -5
  90. package/dist/runtime/openclaw/moltnet.d.ts +12 -0
  91. package/dist/runtime/openclaw/moltnet.js +124 -0
  92. package/dist/runtime/openclaw/surfaces.js +3 -0
  93. package/dist/runtime/picoclaw/adapter.js +27 -8
  94. package/dist/runtime/picoclaw/pico.d.ts +2 -0
  95. package/dist/runtime/picoclaw/pico.js +2 -0
  96. package/dist/runtime/picoclaw/surfaces.js +11 -0
  97. package/dist/runtime/tinyclaw/adapter.js +22 -8
  98. package/dist/runtime/tinyclaw/runAuth.js +28 -1
  99. package/dist/runtime/tinyclaw/surfaces.js +8 -0
  100. package/dist/runtime/types.d.ts +11 -0
  101. package/package.json +5 -3
  102. package/runtimes.yaml +4 -4
@@ -0,0 +1,208 @@
1
+ import { getRuntimeAdapter } from "../runtime/index.js";
2
+ import { SpawnfileError } from "../shared/index.js";
3
+ import { listConcreteMoltnetRoomMemberIds } from "./moltnetRoomMemberships.js";
4
+ const DEFAULT_MOLTNET_PORT = 8787;
5
+ const DEFAULT_TINYCLAW_PORT = 3777;
6
+ const ROOTFS_PREFIX = "container/rootfs";
7
+ const INSTANCE_ROOT_PLACEHOLDER = "<instance-root>";
8
+ const CONFIG_FILE_PLACEHOLDER = "<config-file>";
9
+ const createServerKey = (networkId) => networkId;
10
+ const createBridgeConfigPath = (teamSlug, networkId, agentId) => `${ROOTFS_PREFIX}/var/lib/spawnfile/moltnet/bridges/${teamSlug}-${networkId}-${agentId}.json`;
11
+ const resolveSequentialRuntimePort = (plan, runtimeName, slug) => {
12
+ const adapter = getRuntimeAdapter(runtimeName);
13
+ const basePort = adapter.container.port;
14
+ if (basePort === undefined) {
15
+ return undefined;
16
+ }
17
+ const runtimeAgents = plan.nodes.filter((node) => node.kind === "agent" && node.runtimeName === runtimeName);
18
+ const index = runtimeAgents.findIndex((node) => node.slug === slug);
19
+ if (index < 0) {
20
+ return undefined;
21
+ }
22
+ return basePort + (index * (adapter.container.portStride ?? 1));
23
+ };
24
+ const createTinyClawChannel = (networkId, agentId) => `moltnet:${networkId}:${agentId}`;
25
+ const replaceContainerPathTemplate = (template, instanceRoot, configFileName) => template
26
+ .replaceAll(INSTANCE_ROOT_PLACEHOLDER, instanceRoot)
27
+ .replaceAll(CONFIG_FILE_PLACEHOLDER, configFileName);
28
+ const resolveRuntimeInstancePaths = (runtimeName, slug) => {
29
+ const adapter = getRuntimeAdapter(runtimeName);
30
+ const instanceRoot = `/var/lib/spawnfile/instances/${runtimeName}/agent-${slug}`;
31
+ return {
32
+ configPath: replaceContainerPathTemplate(adapter.container.instancePaths.configPathTemplate, instanceRoot, adapter.container.configFileName),
33
+ homePath: adapter.container.instancePaths.homePathTemplate
34
+ ? replaceContainerPathTemplate(adapter.container.instancePaths.homePathTemplate, instanceRoot, adapter.container.configFileName)
35
+ : undefined
36
+ };
37
+ };
38
+ const resolveRuntimeConfig = (plan, agentNode, nodeSlug, networkId, agentId) => {
39
+ switch (agentNode.runtime.name) {
40
+ case "openclaw": {
41
+ const port = resolveSequentialRuntimePort(plan, "openclaw", nodeSlug);
42
+ if (!port) {
43
+ throw new SpawnfileError("compile_error", `Unable to resolve OpenClaw gateway port for Moltnet agent ${agentNode.name}`);
44
+ }
45
+ const instancePaths = resolveRuntimeInstancePaths("openclaw", nodeSlug);
46
+ return {
47
+ gateway_url: `ws://127.0.0.1:${port}`,
48
+ ...(instancePaths.homePath ? { home_path: instancePaths.homePath } : {}),
49
+ kind: "openclaw",
50
+ };
51
+ }
52
+ case "picoclaw": {
53
+ const instancePaths = resolveRuntimeInstancePaths("picoclaw", nodeSlug);
54
+ return {
55
+ command: "/usr/local/bin/picoclaw",
56
+ config_path: instancePaths.configPath,
57
+ ...(instancePaths.homePath ? { home_path: instancePaths.homePath } : {}),
58
+ kind: "picoclaw",
59
+ };
60
+ }
61
+ case "tinyclaw": {
62
+ const channel = createTinyClawChannel(networkId, agentId);
63
+ return {
64
+ ack_url: `http://127.0.0.1:${DEFAULT_TINYCLAW_PORT}/api/responses`,
65
+ channel,
66
+ inbound_url: `http://127.0.0.1:${DEFAULT_TINYCLAW_PORT}/api/message`,
67
+ kind: "tinyclaw",
68
+ outbound_url: `http://127.0.0.1:${DEFAULT_TINYCLAW_PORT}/api/responses/pending?channel=${encodeURIComponent(channel)}`
69
+ };
70
+ }
71
+ default:
72
+ throw new SpawnfileError("compile_error", `Moltnet does not know how to attach runtime ${agentNode.runtime.name} directly`);
73
+ }
74
+ };
75
+ export const generateMoltnetArtifacts = async (plan) => {
76
+ const teamNodes = plan.nodes
77
+ .filter((node) => node.kind === "team")
78
+ .filter((node) => (node.value.networks?.length ?? 0) > 0);
79
+ if (teamNodes.length === 0) {
80
+ return null;
81
+ }
82
+ const serverPlans = new Map();
83
+ let nextPort = DEFAULT_MOLTNET_PORT;
84
+ for (const teamNode of teamNodes) {
85
+ for (const network of teamNode.value.networks ?? []) {
86
+ const serverKey = createServerKey(network.id);
87
+ const existingPlan = serverPlans.get(serverKey);
88
+ if (existingPlan) {
89
+ for (const room of network.rooms) {
90
+ const concreteMembers = listConcreteMoltnetRoomMemberIds(plan, teamNode.value, network.id, room);
91
+ const existingRoom = existingPlan.rooms.find((entry) => entry.id === room.id);
92
+ if (existingRoom) {
93
+ existingRoom.members = [
94
+ ...new Set([...existingRoom.members, ...concreteMembers])
95
+ ].sort();
96
+ }
97
+ else {
98
+ existingPlan.rooms.push({
99
+ id: room.id,
100
+ members: concreteMembers
101
+ });
102
+ }
103
+ }
104
+ existingPlan.rooms.sort((left, right) => left.id.localeCompare(right.id));
105
+ }
106
+ else {
107
+ serverPlans.set(serverKey, {
108
+ id: `${teamNode.slug}-${network.id}`,
109
+ name: network.name,
110
+ networkId: network.id,
111
+ port: nextPort,
112
+ rooms: network.rooms.map((room) => ({
113
+ id: room.id,
114
+ members: listConcreteMoltnetRoomMemberIds(plan, teamNode.value, network.id, room)
115
+ })),
116
+ teamSource: teamNode.value.source
117
+ });
118
+ nextPort += 1;
119
+ }
120
+ }
121
+ }
122
+ const bridgePlans = [];
123
+ const bridgePlanKeys = new Set();
124
+ const configFiles = [];
125
+ for (const node of plan.nodes) {
126
+ if (node.kind !== "agent") {
127
+ continue;
128
+ }
129
+ const agentNode = node.value;
130
+ if (!agentNode.surfaces?.moltnet || agentNode.surfaces.moltnet.length === 0) {
131
+ continue;
132
+ }
133
+ for (const attachment of agentNode.surfaces.moltnet) {
134
+ if (!attachment.teamSource || !attachment.memberId) {
135
+ throw new SpawnfileError("validation_error", `Agent ${agentNode.name} Moltnet attachments require a team-bound network context`);
136
+ }
137
+ const teamNode = teamNodes.find((team) => team.value.source === attachment.teamSource);
138
+ if (!teamNode) {
139
+ throw new SpawnfileError("validation_error", `Unable to find team context for Moltnet attachment ${attachment.network} on ${agentNode.name}`);
140
+ }
141
+ const serverPlan = serverPlans.get(createServerKey(attachment.network));
142
+ if (!serverPlan) {
143
+ throw new SpawnfileError("validation_error", `Unable to find Moltnet network ${attachment.network} for ${agentNode.name}`);
144
+ }
145
+ const configPath = createBridgeConfigPath(teamNode.slug, attachment.network, attachment.memberId);
146
+ const bridgePlanKey = `${attachment.network}::${attachment.memberId}`;
147
+ if (bridgePlanKeys.has(bridgePlanKey)) {
148
+ throw new SpawnfileError("validation_error", `Duplicate Moltnet bridge attachment for ${attachment.network}/${attachment.memberId}`);
149
+ }
150
+ bridgePlanKeys.add(bridgePlanKey);
151
+ configFiles.push({
152
+ content: `${JSON.stringify({
153
+ version: "moltnet.bridge.v1",
154
+ agent: {
155
+ id: attachment.memberId,
156
+ name: agentNode.name
157
+ },
158
+ moltnet: {
159
+ base_url: `http://127.0.0.1:${serverPlan.port}`,
160
+ network_id: attachment.network
161
+ },
162
+ runtime: resolveRuntimeConfig(plan, agentNode, node.slug, attachment.network, attachment.memberId),
163
+ ...(attachment.rooms
164
+ ? {
165
+ rooms: Object.entries(attachment.rooms)
166
+ .sort(([left], [right]) => left.localeCompare(right))
167
+ .map(([roomId, policy]) => ({
168
+ id: roomId,
169
+ ...(policy.read ? { read: policy.read } : {}),
170
+ ...(policy.reply ? { reply: policy.reply } : {})
171
+ }))
172
+ }
173
+ : {}),
174
+ ...(attachment.dms
175
+ ? {
176
+ dms: {
177
+ enabled: attachment.dms.enabled,
178
+ ...(attachment.dms.read ? { read: attachment.dms.read } : {}),
179
+ ...(attachment.dms.reply ? { reply: attachment.dms.reply } : {})
180
+ }
181
+ }
182
+ : {})
183
+ }, null, 2)}\n`,
184
+ mode: 0o600,
185
+ path: configPath
186
+ });
187
+ bridgePlans.push({
188
+ agentId: attachment.memberId,
189
+ configPath: `/${configPath.replace(`${ROOTFS_PREFIX}/`, "")}`,
190
+ networkId: attachment.network,
191
+ runtime: agentNode.runtime.name
192
+ });
193
+ }
194
+ }
195
+ return {
196
+ bridgePlans: bridgePlans.sort((left, right) => left.configPath.localeCompare(right.configPath)),
197
+ files: configFiles,
198
+ ports: [...new Set([...serverPlans.values()].map((plan) => plan.port))].sort((left, right) => left - right),
199
+ publishedPorts: [
200
+ ...new Set(teamNodes
201
+ .flatMap((teamNode) => (teamNode.value.networks ?? []).map((network) => network.expose
202
+ ? serverPlans.get(createServerKey(network.id))?.port
203
+ : undefined))
204
+ .filter((port) => port !== undefined))
205
+ ].sort((left, right) => left - right),
206
+ serverPlans: [...serverPlans.values()].sort((left, right) => left.port - right.port)
207
+ };
208
+ };
@@ -0,0 +1,4 @@
1
+ export declare const MOLTNET_BIN_DIRECTORY = "moltnet-bin";
2
+ export declare const MOLTNET_BINARY_NAMES: readonly ["moltnet"];
3
+ export declare const resolveMoltnetCliCommand: () => Promise<string>;
4
+ export declare const stageMoltnetBinaries: (outputDirectory: string) => Promise<boolean>;
@@ -0,0 +1,103 @@
1
+ import { execFile as execFileCallback } from "node:child_process";
2
+ import path from "node:path";
3
+ import { chmod } from "node:fs/promises";
4
+ import { promisify } from "node:util";
5
+ import { ensureDirectory, fileExists } from "../filesystem/index.js";
6
+ import { SpawnfileError } from "../shared/index.js";
7
+ const execFile = promisify(execFileCallback);
8
+ const MOLTNET_CLI_ENV = "SPAWNFILE_MOLTNET_CLI";
9
+ const MOLTNET_RELEASE_DIR_ENV = "SPAWNFILE_MOLTNET_RELEASE_DIR";
10
+ const LOCAL_MOLTNET_CLI_PATH = path.resolve(process.cwd(), "moltnet", "bin", "moltnet");
11
+ const MOLTNET_TARGET_OS = "linux";
12
+ export const MOLTNET_BIN_DIRECTORY = "moltnet-bin";
13
+ export const MOLTNET_BINARY_NAMES = ["moltnet"];
14
+ const resolveTargetArchitecture = () => {
15
+ switch (process.arch) {
16
+ case "arm64":
17
+ return "arm64";
18
+ case "x64":
19
+ return "amd64";
20
+ default:
21
+ throw new SpawnfileError("compile_error", `Moltnet container installs do not support host architecture ${process.arch}`);
22
+ }
23
+ };
24
+ const createReleaseAssetName = (architecture) => `moltnet_${MOLTNET_TARGET_OS}_${architecture}.tar.gz`;
25
+ const isCommandNotFoundError = (error) => typeof error === "object" &&
26
+ error !== null &&
27
+ "code" in error &&
28
+ error.code === "ENOENT";
29
+ const validateMoltnetCli = async (command, sourceLabel) => {
30
+ try {
31
+ await execFile(command, ["version"]);
32
+ return command;
33
+ }
34
+ catch (error) {
35
+ const reason = error instanceof Error ? error.message : String(error);
36
+ throw new SpawnfileError("compile_error", `Unable to execute compiled Moltnet CLI from ${sourceLabel}: ${reason}. Install Moltnet with \`curl -fsSL https://moltnet.dev/install.sh | sh\` or set ${MOLTNET_CLI_ENV}.`);
37
+ }
38
+ };
39
+ const resolveConfiguredReleaseDirectory = async () => {
40
+ const configuredDirectory = process.env[MOLTNET_RELEASE_DIR_ENV]?.trim();
41
+ if (!configuredDirectory) {
42
+ return null;
43
+ }
44
+ if (!(await fileExists(configuredDirectory))) {
45
+ throw new SpawnfileError("compile_error", `Moltnet release directory ${configuredDirectory} does not exist`);
46
+ }
47
+ return configuredDirectory;
48
+ };
49
+ const findPathMoltnetCli = async () => {
50
+ try {
51
+ await execFile("moltnet", ["version"]);
52
+ return "moltnet";
53
+ }
54
+ catch (error) {
55
+ if (isCommandNotFoundError(error)) {
56
+ return null;
57
+ }
58
+ const reason = error instanceof Error ? error.message : String(error);
59
+ throw new SpawnfileError("compile_error", `Unable to execute compiled Moltnet CLI from PATH: ${reason}. Install Moltnet with \`curl -fsSL https://moltnet.dev/install.sh | sh\` or set ${MOLTNET_CLI_ENV}.`);
60
+ }
61
+ };
62
+ export const resolveMoltnetCliCommand = async () => {
63
+ const configuredCli = process.env[MOLTNET_CLI_ENV]?.trim();
64
+ if (configuredCli) {
65
+ return validateMoltnetCli(configuredCli, configuredCli);
66
+ }
67
+ const pathCli = await findPathMoltnetCli();
68
+ if (pathCli) {
69
+ return pathCli;
70
+ }
71
+ if (await fileExists(LOCAL_MOLTNET_CLI_PATH)) {
72
+ return validateMoltnetCli(LOCAL_MOLTNET_CLI_PATH, LOCAL_MOLTNET_CLI_PATH);
73
+ }
74
+ return validateMoltnetCli("moltnet", "PATH");
75
+ };
76
+ export const stageMoltnetBinaries = async (outputDirectory) => {
77
+ const releaseDirectory = await resolveConfiguredReleaseDirectory();
78
+ if (!releaseDirectory) {
79
+ return false;
80
+ }
81
+ const architecture = resolveTargetArchitecture();
82
+ const releaseAssetPath = path.join(releaseDirectory, createReleaseAssetName(architecture));
83
+ if (!(await fileExists(releaseAssetPath))) {
84
+ throw new SpawnfileError("compile_error", `Moltnet release asset ${releaseAssetPath} does not exist. Build it with \`cd moltnet && make release-assets\` or set ${MOLTNET_RELEASE_DIR_ENV}.`);
85
+ }
86
+ const installDirectory = path.join(outputDirectory, MOLTNET_BIN_DIRECTORY);
87
+ await ensureDirectory(installDirectory);
88
+ try {
89
+ await execFile("tar", ["-C", installDirectory, "-xzf", releaseAssetPath]);
90
+ }
91
+ catch (error) {
92
+ const reason = error instanceof Error ? error.message : String(error);
93
+ throw new SpawnfileError("compile_error", `Unable to extract Moltnet release asset ${releaseAssetPath}: ${reason}`);
94
+ }
95
+ for (const binaryName of MOLTNET_BINARY_NAMES) {
96
+ const binaryPath = path.join(installDirectory, binaryName);
97
+ if (!(await fileExists(binaryPath))) {
98
+ throw new SpawnfileError("compile_error", `Moltnet release asset ${releaseAssetPath} did not contain ${binaryName}`);
99
+ }
100
+ await chmod(binaryPath, 0o755);
101
+ }
102
+ return true;
103
+ };
@@ -0,0 +1,11 @@
1
+ import type { EmittedFile } from "../runtime/index.js";
2
+ import type { MoltnetArtifacts } from "./moltnetArtifacts.js";
3
+ import type { ResolvedAgentNode } from "./types.js";
4
+ export interface MoltnetWorkspaceLayout {
5
+ clientConfigPath: string;
6
+ cliRuntime: string;
7
+ skillPaths: string[];
8
+ workspaceRootPath: string;
9
+ }
10
+ export declare const resolveMoltnetWorkspaceLayout: (runtimeName: string, agentName: string) => MoltnetWorkspaceLayout;
11
+ export declare const createMoltnetClientConfigFiles: (node: ResolvedAgentNode, artifacts: MoltnetArtifacts) => EmittedFile[];
@@ -0,0 +1,89 @@
1
+ import { SpawnfileError } from "../shared/index.js";
2
+ const GENERATED_SKILL_NAME = "moltnet";
3
+ const GENERATED_CONFIG_PATH = ".moltnet/config.json";
4
+ const createConfigContent = (node, attachments) => `${JSON.stringify({
5
+ version: "moltnet.client.v1",
6
+ agent: {
7
+ name: node.name,
8
+ runtime: node.runtime.name
9
+ },
10
+ attachments
11
+ }, null, 2)}\n`;
12
+ const findServerPlan = (artifacts, attachment) => {
13
+ const exactPlan = artifacts.serverPlans.find((serverPlan) => serverPlan.networkId === attachment.network &&
14
+ serverPlan.teamSource === attachment.teamSource);
15
+ const plan = exactPlan ?? artifacts.serverPlans.find((serverPlan) => serverPlan.networkId === attachment.network);
16
+ if (!plan) {
17
+ throw new SpawnfileError("compile_error", `Unable to resolve Moltnet server plan for ${attachment.network} on ${attachment.memberId ?? "unknown-agent"}`);
18
+ }
19
+ return plan;
20
+ };
21
+ const createAttachmentConfig = (node, artifacts, attachment) => {
22
+ if (!attachment.memberId) {
23
+ throw new SpawnfileError("compile_error", `Moltnet client config requires a resolved member id for ${node.name}`);
24
+ }
25
+ const serverPlan = findServerPlan(artifacts, attachment);
26
+ return {
27
+ agent_name: node.name,
28
+ auth: { mode: "none" },
29
+ base_url: `http://127.0.0.1:${serverPlan.port}`,
30
+ ...(attachment.dms
31
+ ? {
32
+ dms: {
33
+ enabled: attachment.dms.enabled,
34
+ ...(attachment.dms.read ? { read: attachment.dms.read } : {}),
35
+ ...(attachment.dms.reply ? { reply: attachment.dms.reply } : {})
36
+ }
37
+ }
38
+ : {}),
39
+ member_id: attachment.memberId,
40
+ network_id: attachment.network,
41
+ runtime: node.runtime.name,
42
+ ...(attachment.rooms
43
+ ? {
44
+ rooms: Object.entries(attachment.rooms)
45
+ .sort(([left], [right]) => left.localeCompare(right))
46
+ .map(([roomId, policy]) => ({
47
+ id: roomId,
48
+ ...(policy.read ? { read: policy.read } : {}),
49
+ ...(policy.reply ? { reply: policy.reply } : {})
50
+ }))
51
+ }
52
+ : {})
53
+ };
54
+ };
55
+ export const resolveMoltnetWorkspaceLayout = (runtimeName, agentName) => {
56
+ if (runtimeName === "openclaw" || runtimeName === "picoclaw") {
57
+ return {
58
+ clientConfigPath: `workspace/${GENERATED_CONFIG_PATH}`,
59
+ cliRuntime: runtimeName,
60
+ skillPaths: [`workspace/skills/${GENERATED_SKILL_NAME}/SKILL.md`],
61
+ workspaceRootPath: "workspace"
62
+ };
63
+ }
64
+ if (runtimeName === "tinyclaw") {
65
+ return {
66
+ clientConfigPath: `workspace/${agentName}/${GENERATED_CONFIG_PATH}`,
67
+ cliRuntime: runtimeName,
68
+ skillPaths: [
69
+ `workspace/${agentName}/.agents/skills/${GENERATED_SKILL_NAME}/SKILL.md`,
70
+ `workspace/${agentName}/.claude/skills/${GENERATED_SKILL_NAME}/SKILL.md`
71
+ ],
72
+ workspaceRootPath: `workspace/${agentName}`
73
+ };
74
+ }
75
+ throw new SpawnfileError("compile_error", `Moltnet client config does not know how to emit files for runtime ${runtimeName}`);
76
+ };
77
+ export const createMoltnetClientConfigFiles = (node, artifacts) => {
78
+ const attachments = node.surfaces?.moltnet;
79
+ if (!attachments || attachments.length === 0) {
80
+ return [];
81
+ }
82
+ const layout = resolveMoltnetWorkspaceLayout(node.runtime.name, node.name);
83
+ return [
84
+ {
85
+ content: createConfigContent(node, attachments.map((attachment) => createAttachmentConfig(node, artifacts, attachment))),
86
+ path: layout.clientConfigPath
87
+ }
88
+ ];
89
+ };
@@ -0,0 +1,16 @@
1
+ import type { CompilePlan, ResolvedMoltnetAttachment, ResolvedTeamNetwork, ResolvedTeamNode } from "./types.js";
2
+ export interface MoltnetTeamContext {
3
+ memberId: string;
4
+ teamName: string;
5
+ teamSource: string;
6
+ networks: ResolvedTeamNetwork[];
7
+ }
8
+ export interface TeamRepresentativeResolution {
9
+ agentSource: string;
10
+ memberId: string;
11
+ path: string[];
12
+ sourceTeamName: string;
13
+ sourceTeamSource: string;
14
+ }
15
+ export declare const resolveTeamRepresentatives: (plan: CompilePlan, teamNode: ResolvedTeamNode, seen?: string[]) => TeamRepresentativeResolution[];
16
+ export declare const resolveMoltnetAttachments: (attachments: ResolvedMoltnetAttachment[] | undefined, context: MoltnetTeamContext | undefined, nodeName: string) => ResolvedMoltnetAttachment[] | undefined;
@@ -0,0 +1,86 @@
1
+ import { SpawnfileError } from "../shared/index.js";
2
+ const cloneAttachment = (attachment, context) => ({
3
+ ...(attachment.dms ? { dms: { ...attachment.dms } } : {}),
4
+ memberId: context.memberId,
5
+ network: attachment.network,
6
+ ...(attachment.rooms
7
+ ? {
8
+ rooms: Object.fromEntries(Object.entries(attachment.rooms).map(([roomId, policy]) => [
9
+ roomId,
10
+ { ...policy }
11
+ ]))
12
+ }
13
+ : {}),
14
+ teamSource: context.teamSource
15
+ });
16
+ const findTeamBySource = (plan, source) => {
17
+ const node = plan.nodes.find((entry) => entry.kind === "team" && entry.value.source === source);
18
+ if (!node || node.value.kind !== "team") {
19
+ throw new SpawnfileError("compile_error", `Unable to find team node at ${source}`);
20
+ }
21
+ return node.value;
22
+ };
23
+ export const resolveTeamRepresentatives = (plan, teamNode, seen = []) => {
24
+ if (seen.includes(teamNode.source)) {
25
+ throw new SpawnfileError("compile_error", `Cycle detected while resolving representatives for ${teamNode.name}`);
26
+ }
27
+ const selectedMemberIds = teamNode.externalExplicit
28
+ ? teamNode.external
29
+ : teamNode.mode === "hierarchical" && teamNode.lead
30
+ ? [teamNode.lead]
31
+ : teamNode.members.map((member) => member.id);
32
+ const nextSeen = [...seen, teamNode.source];
33
+ const representatives = [];
34
+ for (const memberId of selectedMemberIds) {
35
+ const member = teamNode.members.find((entry) => entry.id === memberId);
36
+ if (!member) {
37
+ throw new SpawnfileError("validation_error", `Team ${teamNode.name} representative interface references unknown member ${memberId}`);
38
+ }
39
+ if (member.kind === "agent") {
40
+ representatives.push({
41
+ agentSource: member.nodeSource,
42
+ memberId: member.id,
43
+ path: [member.id],
44
+ sourceTeamName: teamNode.name,
45
+ sourceTeamSource: teamNode.source
46
+ });
47
+ continue;
48
+ }
49
+ const childTeam = findTeamBySource(plan, member.nodeSource);
50
+ const childRepresentatives = resolveTeamRepresentatives(plan, childTeam, nextSeen);
51
+ if (childRepresentatives.length === 0) {
52
+ throw new SpawnfileError("validation_error", `Team ${childTeam.name} does not resolve to a concrete representative for ${teamNode.name}`);
53
+ }
54
+ for (const representative of childRepresentatives) {
55
+ representatives.push({
56
+ ...representative,
57
+ path: [member.id, ...representative.path]
58
+ });
59
+ }
60
+ }
61
+ return representatives;
62
+ };
63
+ export const resolveMoltnetAttachments = (attachments, context, nodeName) => {
64
+ if (!attachments || attachments.length === 0) {
65
+ return undefined;
66
+ }
67
+ if (!context) {
68
+ throw new SpawnfileError("validation_error", `Agent ${nodeName} declares Moltnet surfaces but is not attached to a team network`);
69
+ }
70
+ return attachments.map((attachment) => {
71
+ const network = context.networks.find((entry) => entry.id === attachment.network);
72
+ if (!network) {
73
+ throw new SpawnfileError("validation_error", `Agent ${nodeName} references unknown Moltnet network ${attachment.network} on team ${context.teamName}`);
74
+ }
75
+ for (const roomId of Object.keys(attachment.rooms ?? {})) {
76
+ const room = network.rooms.find((entry) => entry.id === roomId);
77
+ if (!room) {
78
+ throw new SpawnfileError("validation_error", `Agent ${nodeName} references unknown Moltnet room ${roomId} on network ${network.id}`);
79
+ }
80
+ if (!room.members.includes(context.memberId)) {
81
+ throw new SpawnfileError("validation_error", `Agent ${nodeName} cannot attach to Moltnet room ${roomId} because member ${context.memberId} is not in that room`);
82
+ }
83
+ }
84
+ return cloneAttachment(attachment, context);
85
+ });
86
+ };
@@ -0,0 +1,3 @@
1
+ import type { CompilePlan } from "./types.js";
2
+ export { resolveMoltnetAttachments, resolveTeamRepresentatives, type MoltnetTeamContext, type TeamRepresentativeResolution } from "./moltnetRepresentativeResolution.js";
3
+ export declare const resolvePlanMoltnetAttachments: (plan: CompilePlan) => void;