@teamclaws/teamclaw 2026.3.26-2 → 2026.4.2-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.
- package/README.md +52 -8
- package/cli.mjs +538 -224
- package/index.ts +76 -27
- package/openclaw.plugin.json +53 -28
- package/package.json +5 -2
- package/skills/teamclaw/SKILL.md +213 -0
- package/skills/teamclaw/references/api-quick-ref.md +117 -0
- package/skills/teamclaw-setup/SKILL.md +81 -0
- package/skills/teamclaw-setup/references/install-modes.md +136 -0
- package/skills/teamclaw-setup/references/validation-checklist.md +73 -0
- package/src/config.ts +44 -16
- package/src/controller/controller-capacity.ts +2 -2
- package/src/controller/controller-service.ts +193 -47
- package/src/controller/controller-tools.ts +102 -2
- package/src/controller/delivery-report.ts +563 -0
- package/src/controller/http-server.ts +1907 -172
- package/src/controller/kickoff-orchestrator.ts +292 -0
- package/src/controller/managed-gateway-process.ts +330 -0
- package/src/controller/orchestration-manifest.ts +69 -1
- package/src/controller/preview-manager.ts +676 -0
- package/src/controller/prompt-injector.ts +116 -67
- package/src/controller/role-inference.ts +41 -0
- package/src/controller/websocket.ts +3 -1
- package/src/controller/worker-provisioning.ts +429 -74
- package/src/discovery.ts +1 -1
- package/src/git-collaboration.ts +198 -47
- package/src/identity.ts +12 -2
- package/src/interaction-contracts.ts +179 -3
- package/src/networking.ts +99 -0
- package/src/openclaw-workspace.ts +478 -11
- package/src/prompt-policy.ts +381 -0
- package/src/roles.ts +37 -36
- package/src/state.ts +40 -1
- package/src/task-executor.ts +282 -78
- package/src/types.ts +150 -7
- package/src/ui/app.js +1403 -175
- package/src/ui/assets/teamclaw-app-icon.png +0 -0
- package/src/ui/index.html +122 -40
- package/src/ui/style.css +829 -143
- package/src/worker/http-handler.ts +40 -4
- package/src/worker/prompt-injector.ts +9 -38
- package/src/worker/skill-installer.ts +2 -2
- package/src/worker/tools.ts +31 -5
- package/src/worker/worker-service.ts +49 -8
- package/src/workspace-browser.ts +20 -7
- package/src/controller/local-worker-manager.ts +0 -533
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
function isPrivateLanIpv4(address: string): boolean {
|
|
5
|
+
if (address.startsWith("10.") || address.startsWith("192.168.")) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
const parts = address.split(".").map((value) => Number.parseInt(value, 10));
|
|
9
|
+
return parts.length === 4 && parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function rankLanAddress(address: string): number {
|
|
13
|
+
if (address.startsWith("192.168.")) {
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
if (address.startsWith("10.")) {
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
const parts = address.split(".").map((value) => Number.parseInt(value, 10));
|
|
20
|
+
if (parts.length === 4 && parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) {
|
|
21
|
+
return 2;
|
|
22
|
+
}
|
|
23
|
+
return 3;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseDefaultRouteInterface(text: string): string {
|
|
27
|
+
const directMatch = text.match(/(?:^|\n)\s*interface:\s*(\S+)/i);
|
|
28
|
+
if (directMatch?.[1]) {
|
|
29
|
+
return directMatch[1];
|
|
30
|
+
}
|
|
31
|
+
const devMatch = text.match(/(?:^|\n)default(?:\s+via\s+\S+)?\s+dev\s+(\S+)/i);
|
|
32
|
+
if (devMatch?.[1]) {
|
|
33
|
+
return devMatch[1];
|
|
34
|
+
}
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function probeDefaultRouteInterface(): string {
|
|
39
|
+
const candidates: Array<{ command: string; args: string[] }> = process.platform === "darwin"
|
|
40
|
+
? [
|
|
41
|
+
{ command: "route", args: ["-n", "get", "default"] },
|
|
42
|
+
{ command: "ip", args: ["route", "show", "default"] },
|
|
43
|
+
]
|
|
44
|
+
: [
|
|
45
|
+
{ command: "ip", args: ["route", "show", "default"] },
|
|
46
|
+
{ command: "route", args: ["-n", "get", "default"] },
|
|
47
|
+
];
|
|
48
|
+
for (const candidate of candidates) {
|
|
49
|
+
const result = spawnSync(candidate.command, candidate.args, { encoding: "utf8" });
|
|
50
|
+
if (result.status !== 0 || result.error) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const interfaceName = parseDefaultRouteInterface(result.stdout || "");
|
|
54
|
+
if (interfaceName) {
|
|
55
|
+
return interfaceName;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function listNonInternalIpv4Addresses(): string[] {
|
|
62
|
+
const addresses: string[] = [];
|
|
63
|
+
const interfaces = os.networkInterfaces();
|
|
64
|
+
for (const records of Object.values(interfaces)) {
|
|
65
|
+
for (const record of records ?? []) {
|
|
66
|
+
if (!record || record.internal || record.family !== "IPv4") {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
addresses.push(record.address);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return addresses.filter((value, index, values) => values.indexOf(value) === index);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function resolvePreferredLanAddress(): string | null {
|
|
76
|
+
const interfaces = os.networkInterfaces();
|
|
77
|
+
const defaultRouteInterface = probeDefaultRouteInterface();
|
|
78
|
+
if (defaultRouteInterface) {
|
|
79
|
+
const records = (interfaces[defaultRouteInterface] ?? [])
|
|
80
|
+
.filter((record): record is os.NetworkInterfaceInfoIPv4 => Boolean(record) && record.family === "IPv4" && !record.internal);
|
|
81
|
+
const preferredPrivate = records.find((record) => isPrivateLanIpv4(record.address));
|
|
82
|
+
if (preferredPrivate) {
|
|
83
|
+
return preferredPrivate.address;
|
|
84
|
+
}
|
|
85
|
+
if (records[0]) {
|
|
86
|
+
return records[0].address;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const privateCandidates = listNonInternalIpv4Addresses()
|
|
91
|
+
.filter((address) => isPrivateLanIpv4(address))
|
|
92
|
+
.sort((left, right) => rankLanAddress(left) - rankLanAddress(right) || left.localeCompare(right));
|
|
93
|
+
if (privateCandidates[0]) {
|
|
94
|
+
return privateCandidates[0];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const fallback = listNonInternalIpv4Addresses()[0];
|
|
98
|
+
return fallback || null;
|
|
99
|
+
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import JSON5 from "json5";
|
|
6
6
|
import type { PluginLogger } from "../api.js";
|
|
7
7
|
|
|
8
|
+
export const TEAMCLAW_AGENT_ID = "teamclaw";
|
|
9
|
+
export const TEAMCLAW_ISOLATION_MODE_INDEPENDENT = "independent";
|
|
10
|
+
export const TEAMCLAW_ISOLATION_MODE_MAIN = "main";
|
|
11
|
+
const TEAMCLAW_RECOMMENDED_EXEC_SECURITY = "full";
|
|
12
|
+
const TEAMCLAW_RECOMMENDED_EXEC_ASK = "off";
|
|
13
|
+
|
|
8
14
|
const DEFAULT_AGENTS_MD = `# AGENTS.md
|
|
9
15
|
|
|
10
16
|
This workspace is shared by TeamClaw controller and workers.
|
|
@@ -12,6 +18,8 @@ This workspace is shared by TeamClaw controller and workers.
|
|
|
12
18
|
Rules:
|
|
13
19
|
- Treat task-provided file paths as hints; verify they exist before reading or editing.
|
|
14
20
|
- Use the shared \`memory/\` directory for lightweight notes when useful.
|
|
21
|
+
- Check \`memory/patterns.md\` for previously discovered codebase patterns before starting work.
|
|
22
|
+
- Check for \`.teamclaw-notes.md\` files in directories you work on for prior context.
|
|
15
23
|
- Report meaningful progress during longer tasks.
|
|
16
24
|
- If requirements or environment details are missing and work cannot continue safely, request clarification instead of guessing.
|
|
17
25
|
`;
|
|
@@ -31,10 +39,25 @@ const DEFAULT_HEARTBEAT_MD = `# HEARTBEAT.md
|
|
|
31
39
|
# Keep this file empty (or with only comments) to skip heartbeat API calls.
|
|
32
40
|
`;
|
|
33
41
|
|
|
42
|
+
const DEFAULT_PATTERNS_MD = `# Codebase Patterns
|
|
43
|
+
|
|
44
|
+
Reusable patterns discovered by TeamClaw workers during task execution.
|
|
45
|
+
This file is automatically maintained — new patterns are appended as workers complete tasks.
|
|
46
|
+
Read this file before starting work to benefit from previously discovered knowledge.
|
|
47
|
+
`;
|
|
48
|
+
|
|
34
49
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
35
50
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
36
51
|
}
|
|
37
52
|
|
|
53
|
+
function normalizeAgentId(value: unknown): string {
|
|
54
|
+
const trimmed = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
55
|
+
if (!trimmed) {
|
|
56
|
+
return "main";
|
|
57
|
+
}
|
|
58
|
+
return trimmed.replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "main";
|
|
59
|
+
}
|
|
60
|
+
|
|
38
61
|
function expandUserPath(
|
|
39
62
|
value: string,
|
|
40
63
|
homedir: () => string = os.homedir,
|
|
@@ -56,24 +79,139 @@ function resolveConfiguredOpenClawWorkspaceDir(
|
|
|
56
79
|
env: NodeJS.ProcessEnv = process.env,
|
|
57
80
|
homedir: () => string = os.homedir,
|
|
58
81
|
): string {
|
|
82
|
+
const parsed = loadOpenClawConfigRecord(env, homedir);
|
|
83
|
+
if (!parsed) {
|
|
84
|
+
return "";
|
|
85
|
+
}
|
|
86
|
+
const agents = isRecord(parsed.agents) ? parsed.agents : null;
|
|
87
|
+
const defaults = agents && isRecord(agents.defaults) ? agents.defaults : null;
|
|
88
|
+
if (defaults && typeof defaults.workspace === "string" && defaults.workspace.trim()) {
|
|
89
|
+
return expandUserPath(defaults.workspace, homedir);
|
|
90
|
+
}
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveConfiguredTeamClawIsolationMode(
|
|
95
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
96
|
+
homedir: () => string = os.homedir,
|
|
97
|
+
): string {
|
|
98
|
+
const parsed = loadOpenClawConfigRecord(env, homedir);
|
|
99
|
+
if (!parsed) {
|
|
100
|
+
return TEAMCLAW_ISOLATION_MODE_INDEPENDENT;
|
|
101
|
+
}
|
|
102
|
+
const plugins = isRecord(parsed.plugins) ? parsed.plugins : null;
|
|
103
|
+
const entries = plugins && isRecord(plugins.entries) ? plugins.entries : null;
|
|
104
|
+
const teamclawEntry = entries && isRecord(entries[TEAMCLAW_AGENT_ID]) ? entries[TEAMCLAW_AGENT_ID] : null;
|
|
105
|
+
const teamclawConfig = teamclawEntry && isRecord(teamclawEntry.config) ? teamclawEntry.config : null;
|
|
106
|
+
return teamclawConfig?.agentIsolationMode === TEAMCLAW_ISOLATION_MODE_MAIN
|
|
107
|
+
? TEAMCLAW_ISOLATION_MODE_MAIN
|
|
108
|
+
: TEAMCLAW_ISOLATION_MODE_INDEPENDENT;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveExplicitTeamClawWorkspaceDir(
|
|
112
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
113
|
+
homedir: () => string = os.homedir,
|
|
114
|
+
): string {
|
|
115
|
+
const override = env.TEAMCLAW_WORKSPACE_DIR?.trim();
|
|
116
|
+
if (!override) {
|
|
117
|
+
return "";
|
|
118
|
+
}
|
|
119
|
+
return expandUserPath(override, homedir);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function loadOpenClawConfigRecord(
|
|
123
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
124
|
+
homedir: () => string = os.homedir,
|
|
125
|
+
): Record<string, unknown> | null {
|
|
59
126
|
const configPath = resolveDefaultOpenClawConfigPath(env, homedir);
|
|
60
127
|
try {
|
|
61
128
|
const raw = readFileSync(configPath, "utf8");
|
|
62
129
|
const parsed = JSON5.parse(raw);
|
|
63
|
-
|
|
64
|
-
|
|
130
|
+
return isRecord(parsed) ? parsed : null;
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function resolveConfiguredAgentEntry(
|
|
137
|
+
agentId: string,
|
|
138
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
139
|
+
homedir: () => string = os.homedir,
|
|
140
|
+
): Record<string, unknown> | null {
|
|
141
|
+
const parsed = loadOpenClawConfigRecord(env, homedir);
|
|
142
|
+
if (!parsed) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const agents = isRecord(parsed.agents) ? parsed.agents : null;
|
|
146
|
+
const list = agents && Array.isArray(agents.list) ? agents.list : [];
|
|
147
|
+
const normalizedAgentId = normalizeAgentId(agentId);
|
|
148
|
+
for (const entry of list) {
|
|
149
|
+
if (!isRecord(entry)) {
|
|
150
|
+
continue;
|
|
65
151
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (defaults && typeof defaults.workspace === "string" && defaults.workspace.trim()) {
|
|
69
|
-
return expandUserPath(defaults.workspace, homedir);
|
|
152
|
+
if (normalizeAgentId(entry.id) === normalizedAgentId) {
|
|
153
|
+
return entry;
|
|
70
154
|
}
|
|
71
|
-
}
|
|
72
|
-
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function resolveConfiguredDefaultModelValue(
|
|
160
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
161
|
+
homedir: () => string = os.homedir,
|
|
162
|
+
): string {
|
|
163
|
+
const parsed = loadOpenClawConfigRecord(env, homedir);
|
|
164
|
+
if (!parsed) {
|
|
165
|
+
return "";
|
|
166
|
+
}
|
|
167
|
+
const agents = isRecord(parsed.agents) ? parsed.agents : null;
|
|
168
|
+
const defaults = agents && isRecord(agents.defaults) ? agents.defaults : null;
|
|
169
|
+
const model = defaults?.model;
|
|
170
|
+
if (typeof model === "string") {
|
|
171
|
+
return model.trim();
|
|
172
|
+
}
|
|
173
|
+
if (isRecord(model) && typeof model.primary === "string") {
|
|
174
|
+
return model.primary.trim();
|
|
73
175
|
}
|
|
74
176
|
return "";
|
|
75
177
|
}
|
|
76
178
|
|
|
179
|
+
function resolveConfiguredTeamClawModelValue(
|
|
180
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
181
|
+
homedir: () => string = os.homedir,
|
|
182
|
+
): string {
|
|
183
|
+
const teamclawEntry = resolveConfiguredAgentEntry(TEAMCLAW_AGENT_ID, env, homedir);
|
|
184
|
+
const model = teamclawEntry?.model;
|
|
185
|
+
if (typeof model === "string") {
|
|
186
|
+
return model.trim();
|
|
187
|
+
}
|
|
188
|
+
if (isRecord(model) && typeof model.primary === "string") {
|
|
189
|
+
return model.primary.trim();
|
|
190
|
+
}
|
|
191
|
+
return resolveConfiguredDefaultModelValue(env, homedir);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function cloneJsonValue<T>(value: T): T {
|
|
195
|
+
return value == null ? value : JSON.parse(JSON.stringify(value)) as T;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function resolveConfiguredDefaultAgentId(
|
|
199
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
200
|
+
homedir: () => string = os.homedir,
|
|
201
|
+
): string {
|
|
202
|
+
const parsed = loadOpenClawConfigRecord(env, homedir);
|
|
203
|
+
if (!parsed) {
|
|
204
|
+
return "main";
|
|
205
|
+
}
|
|
206
|
+
const agents = isRecord(parsed.agents) ? parsed.agents : null;
|
|
207
|
+
const list = agents && Array.isArray(agents.list) ? agents.list.filter(isRecord) : [];
|
|
208
|
+
if (list.length === 0) {
|
|
209
|
+
return "main";
|
|
210
|
+
}
|
|
211
|
+
const defaultEntry = list.find((entry) => entry.default === true) ?? list[0];
|
|
212
|
+
return normalizeAgentId(defaultEntry.id);
|
|
213
|
+
}
|
|
214
|
+
|
|
77
215
|
export function resolveDefaultOpenClawHomeDir(
|
|
78
216
|
env: NodeJS.ProcessEnv = process.env,
|
|
79
217
|
homedir: () => string = os.homedir,
|
|
@@ -122,15 +260,207 @@ export function resolveDefaultOpenClawWorkspaceDir(
|
|
|
122
260
|
return path.join(stateDir, "workspace");
|
|
123
261
|
}
|
|
124
262
|
|
|
263
|
+
export function resolveTeamClawAgentDir(
|
|
264
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
265
|
+
homedir: () => string = os.homedir,
|
|
266
|
+
): string {
|
|
267
|
+
if (resolveConfiguredTeamClawIsolationMode(env, homedir) === TEAMCLAW_ISOLATION_MODE_MAIN) {
|
|
268
|
+
return resolveDefaultAgentDir(env, homedir);
|
|
269
|
+
}
|
|
270
|
+
const configuredEntry = resolveConfiguredAgentEntry(TEAMCLAW_AGENT_ID, env, homedir);
|
|
271
|
+
if (configuredEntry && typeof configuredEntry.agentDir === "string" && configuredEntry.agentDir.trim()) {
|
|
272
|
+
return expandUserPath(configuredEntry.agentDir, homedir);
|
|
273
|
+
}
|
|
274
|
+
return path.join(resolveDefaultOpenClawStateDir(env, homedir), "agents", TEAMCLAW_AGENT_ID, "agent");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export type TeamClawModelReadiness = {
|
|
278
|
+
status: "ready" | "missing";
|
|
279
|
+
hasConfiguredModel: boolean;
|
|
280
|
+
hasAuthProfiles: boolean;
|
|
281
|
+
configuredModel: string;
|
|
282
|
+
authPath: string;
|
|
283
|
+
message: string;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export function getTeamClawModelReadiness(
|
|
287
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
288
|
+
homedir: () => string = os.homedir,
|
|
289
|
+
): TeamClawModelReadiness {
|
|
290
|
+
const configuredModel = resolveConfiguredTeamClawModelValue(env, homedir);
|
|
291
|
+
const authCandidates = [
|
|
292
|
+
path.join(resolveTeamClawAgentDir(env, homedir), "auth-profiles.json"),
|
|
293
|
+
path.join(resolveDefaultAgentDir(env, homedir), "auth-profiles.json"),
|
|
294
|
+
].filter((value, index, values) => values.indexOf(value) === index);
|
|
295
|
+
const authPath = authCandidates.find((candidatePath) => {
|
|
296
|
+
try {
|
|
297
|
+
return existsSync(candidatePath);
|
|
298
|
+
} catch {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
}) ?? authCandidates[0] ?? "";
|
|
302
|
+
const hasConfiguredModel = Boolean(configuredModel);
|
|
303
|
+
const hasAuthProfiles = Boolean(authPath) && existsSync(authPath);
|
|
304
|
+
const status = hasConfiguredModel && hasAuthProfiles ? "ready" : "missing";
|
|
305
|
+
const missingParts = [];
|
|
306
|
+
if (!hasConfiguredModel) {
|
|
307
|
+
missingParts.push("no model is configured");
|
|
308
|
+
}
|
|
309
|
+
if (!hasAuthProfiles) {
|
|
310
|
+
missingParts.push("no auth-profiles.json was found");
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
status,
|
|
314
|
+
hasConfiguredModel,
|
|
315
|
+
hasAuthProfiles,
|
|
316
|
+
configuredModel,
|
|
317
|
+
authPath,
|
|
318
|
+
message: status === "ready"
|
|
319
|
+
? `TeamClaw is ready with model ${configuredModel}.`
|
|
320
|
+
: `TeamClaw cannot work yet because ${missingParts.join(" and ")}.`,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function resolveDefaultAgentDir(
|
|
325
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
326
|
+
homedir: () => string = os.homedir,
|
|
327
|
+
): string {
|
|
328
|
+
const defaultAgentId = resolveConfiguredDefaultAgentId(env, homedir);
|
|
329
|
+
const configuredEntry = resolveConfiguredAgentEntry(defaultAgentId, env, homedir);
|
|
330
|
+
if (configuredEntry && typeof configuredEntry.agentDir === "string" && configuredEntry.agentDir.trim()) {
|
|
331
|
+
return expandUserPath(configuredEntry.agentDir, homedir);
|
|
332
|
+
}
|
|
333
|
+
return path.join(resolveDefaultOpenClawStateDir(env, homedir), "agents", defaultAgentId, "agent");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function resolveTeamClawAgentWorkspaceRootDir(
|
|
337
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
338
|
+
homedir: () => string = os.homedir,
|
|
339
|
+
): string {
|
|
340
|
+
const explicitWorkspaceDir = resolveExplicitTeamClawWorkspaceDir(env, homedir);
|
|
341
|
+
if (explicitWorkspaceDir) {
|
|
342
|
+
return explicitWorkspaceDir;
|
|
343
|
+
}
|
|
344
|
+
if (resolveConfiguredTeamClawIsolationMode(env, homedir) === TEAMCLAW_ISOLATION_MODE_MAIN) {
|
|
345
|
+
return resolveDefaultOpenClawWorkspaceDir(env, homedir);
|
|
346
|
+
}
|
|
347
|
+
const configuredEntry = resolveConfiguredAgentEntry(TEAMCLAW_AGENT_ID, env, homedir);
|
|
348
|
+
if (configuredEntry && typeof configuredEntry.workspace === "string" && configuredEntry.workspace.trim()) {
|
|
349
|
+
return expandUserPath(configuredEntry.workspace, homedir);
|
|
350
|
+
}
|
|
351
|
+
return path.join(resolveDefaultOpenClawStateDir(env, homedir), `workspace-${TEAMCLAW_AGENT_ID}`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function buildTeamClawAgentSessionKey(
|
|
355
|
+
input: string,
|
|
356
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
357
|
+
homedir: () => string = os.homedir,
|
|
358
|
+
): string {
|
|
359
|
+
const trimmed = input.trim();
|
|
360
|
+
const agentMatch = trimmed.match(/^agent:[^:]+:(.+)$/i);
|
|
361
|
+
const logicalKey = (agentMatch?.[1] ?? trimmed).trim().toLowerCase() || "main";
|
|
362
|
+
if (resolveConfiguredTeamClawIsolationMode(env, homedir) === TEAMCLAW_ISOLATION_MODE_MAIN) {
|
|
363
|
+
return logicalKey;
|
|
364
|
+
}
|
|
365
|
+
return `agent:${TEAMCLAW_AGENT_ID}:${logicalKey}`;
|
|
366
|
+
}
|
|
367
|
+
|
|
125
368
|
export function resolveDefaultTeamClawRuntimeRootDir(
|
|
126
369
|
env: NodeJS.ProcessEnv = process.env,
|
|
127
370
|
homedir: () => string = os.homedir,
|
|
128
371
|
): string {
|
|
129
|
-
return path.join(
|
|
372
|
+
return path.join(resolveDefaultOpenClawStateDir(env, homedir), "teamclaw-runtimes");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* TeamClaw-specific workspace directory.
|
|
377
|
+
*
|
|
378
|
+
* By default this resolves to a dedicated OpenClaw agent workspace sibling such as
|
|
379
|
+
* `<stateDir>/workspace-teamclaw`.
|
|
380
|
+
*
|
|
381
|
+
* When `agentIsolationMode` is set to `main`, TeamClaw falls back to the legacy
|
|
382
|
+
* shared layout and uses `<default-workspace>/teamclaw`.
|
|
383
|
+
*/
|
|
384
|
+
export function resolveTeamClawWorkspaceDir(
|
|
385
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
386
|
+
homedir: () => string = os.homedir,
|
|
387
|
+
): string {
|
|
388
|
+
if (resolveConfiguredTeamClawIsolationMode(env, homedir) === TEAMCLAW_ISOLATION_MODE_MAIN) {
|
|
389
|
+
return path.join(resolveTeamClawAgentWorkspaceRootDir(env, homedir), TEAMCLAW_AGENT_ID);
|
|
390
|
+
}
|
|
391
|
+
return resolveTeamClawAgentWorkspaceRootDir(env, homedir);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Resolve the `projects/` root inside the TeamClaw workspace.
|
|
396
|
+
* Each orchestration run or ad-hoc task gets its own subdirectory here.
|
|
397
|
+
*/
|
|
398
|
+
export function resolveTeamClawProjectsDir(
|
|
399
|
+
env?: NodeJS.ProcessEnv,
|
|
400
|
+
homedir?: () => string,
|
|
401
|
+
): string {
|
|
402
|
+
return path.join(resolveTeamClawWorkspaceDir(env, homedir), "projects");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Resolve a project path relative to the active TeamClaw workspace.
|
|
407
|
+
* Workers operate inside the TeamClaw workspace itself, so project-local file
|
|
408
|
+
* operations should use `projects/<projectDir>` regardless of isolation mode.
|
|
409
|
+
*/
|
|
410
|
+
export function buildTeamClawProjectWorkspacePath(projectDir: string): string {
|
|
411
|
+
const normalized = String(projectDir || "").trim().replace(/^\/+|\/+$/gu, "");
|
|
412
|
+
return normalized ? `projects/${normalized}` : "projects";
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Resolve a project path relative to the TeamClaw agent workspace root.
|
|
417
|
+
* Preview metadata is evaluated from the agent workspace root, which differs
|
|
418
|
+
* between `independent` and `main` isolation modes.
|
|
419
|
+
*/
|
|
420
|
+
export function buildTeamClawProjectAgentRelativePath(
|
|
421
|
+
projectDir: string,
|
|
422
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
423
|
+
homedir: () => string = os.homedir,
|
|
424
|
+
): string {
|
|
425
|
+
const workspaceRelative = buildTeamClawProjectWorkspacePath(projectDir);
|
|
426
|
+
const agentWorkspaceRoot = resolveTeamClawAgentWorkspaceRootDir(env, homedir);
|
|
427
|
+
const teamclawWorkspaceDir = resolveTeamClawWorkspaceDir(env, homedir);
|
|
428
|
+
const absoluteProjectPath = path.join(teamclawWorkspaceDir, workspaceRelative);
|
|
429
|
+
const relativeToAgentRoot = path.relative(agentWorkspaceRoot, absoluteProjectPath).replace(/\\/gu, "/");
|
|
430
|
+
return relativeToAgentRoot || ".";
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Derive a filesystem-safe project slug from free-form text.
|
|
435
|
+
*
|
|
436
|
+
* 1. Lower-case, replace non-alphanumeric runs with hyphens, trim to ~50 chars.
|
|
437
|
+
* 2. Append a short random suffix to avoid collisions.
|
|
438
|
+
*
|
|
439
|
+
* Example: "Build a payment system with Stripe" → "build-a-payment-system-with-stripe-k3f9m2"
|
|
440
|
+
*/
|
|
441
|
+
export function deriveProjectSlug(text: string): string {
|
|
442
|
+
const slug = text
|
|
443
|
+
.toLowerCase()
|
|
444
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
445
|
+
.replace(/^-+|-+$/g, "")
|
|
446
|
+
.slice(0, 50)
|
|
447
|
+
.replace(/-+$/, "");
|
|
448
|
+
const suffix = randomSuffix(6);
|
|
449
|
+
return slug ? `${slug}-${suffix}` : suffix;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function randomSuffix(length: number): string {
|
|
453
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
454
|
+
let result = "";
|
|
455
|
+
for (let i = 0; i < length; i++) {
|
|
456
|
+
result += chars[Math.floor(Math.random() * chars.length)];
|
|
457
|
+
}
|
|
458
|
+
return result;
|
|
130
459
|
}
|
|
131
460
|
|
|
132
461
|
export async function ensureOpenClawWorkspaceMemoryDir(logger: PluginLogger): Promise<string> {
|
|
133
|
-
|
|
462
|
+
await ensureTeamClawAgentBootstrap(logger);
|
|
463
|
+
const workspaceDir = resolveTeamClawWorkspaceDir();
|
|
134
464
|
const memoryDir = path.join(workspaceDir, "memory");
|
|
135
465
|
try {
|
|
136
466
|
await fs.mkdir(workspaceDir, { recursive: true });
|
|
@@ -138,6 +468,7 @@ export async function ensureOpenClawWorkspaceMemoryDir(logger: PluginLogger): Pr
|
|
|
138
468
|
await ensureFileIfMissing(path.join(workspaceDir, "AGENTS.md"), DEFAULT_AGENTS_MD);
|
|
139
469
|
await ensureFileIfMissing(path.join(workspaceDir, "BOOTSTRAP.md"), DEFAULT_BOOTSTRAP_MD);
|
|
140
470
|
await ensureFileIfMissing(path.join(workspaceDir, "HEARTBEAT.md"), DEFAULT_HEARTBEAT_MD);
|
|
471
|
+
await ensureFileIfMissing(path.join(memoryDir, "patterns.md"), DEFAULT_PATTERNS_MD);
|
|
141
472
|
} catch (err) {
|
|
142
473
|
logger.warn(
|
|
143
474
|
`TeamClaw: failed to ensure OpenClaw workspace memory dir at ${memoryDir}: ${
|
|
@@ -148,6 +479,142 @@ export async function ensureOpenClawWorkspaceMemoryDir(logger: PluginLogger): Pr
|
|
|
148
479
|
return memoryDir;
|
|
149
480
|
}
|
|
150
481
|
|
|
482
|
+
export async function ensureTeamClawAgentBootstrap(logger: PluginLogger): Promise<void> {
|
|
483
|
+
const teamclawAgentDir = resolveTeamClawAgentDir();
|
|
484
|
+
const teamclawWorkspaceRoot = resolveTeamClawAgentWorkspaceRootDir();
|
|
485
|
+
const isolationMode = resolveConfiguredTeamClawIsolationMode();
|
|
486
|
+
try {
|
|
487
|
+
await fs.mkdir(teamclawWorkspaceRoot, { recursive: true });
|
|
488
|
+
if (isolationMode === TEAMCLAW_ISOLATION_MODE_MAIN) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
await ensureTeamClawAgentConfigBootstrap(logger);
|
|
492
|
+
await fs.mkdir(teamclawAgentDir, { recursive: true });
|
|
493
|
+
const sourceAgentDirs = [
|
|
494
|
+
resolveDefaultAgentDir(),
|
|
495
|
+
path.join(resolveDefaultOpenClawStateDir(), "agents", "main", "agent"),
|
|
496
|
+
].filter((value, index, values) => values.indexOf(value) === index);
|
|
497
|
+
for (const sourceAgentDir of sourceAgentDirs) {
|
|
498
|
+
const sourceAuthProfilesPath = path.join(sourceAgentDir, "auth-profiles.json");
|
|
499
|
+
try {
|
|
500
|
+
await fs.access(sourceAuthProfilesPath);
|
|
501
|
+
} catch {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const targetAuthProfilesPath = path.join(teamclawAgentDir, "auth-profiles.json");
|
|
505
|
+
await fs.copyFile(sourceAuthProfilesPath, targetAuthProfilesPath);
|
|
506
|
+
logger.info(`TeamClaw: bootstrapped dedicated agent auth from ${sourceAuthProfilesPath}`);
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
} catch (err) {
|
|
510
|
+
logger.warn(
|
|
511
|
+
`TeamClaw: failed to bootstrap dedicated agent runtime: ${err instanceof Error ? err.message : String(err)}`,
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function ensureTeamClawAgentConfigBootstrap(logger: PluginLogger): Promise<void> {
|
|
517
|
+
const configPath = resolveDefaultOpenClawConfigPath();
|
|
518
|
+
let raw: string;
|
|
519
|
+
try {
|
|
520
|
+
raw = await fs.readFile(configPath, "utf8");
|
|
521
|
+
} catch {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
let parsed: Record<string, unknown>;
|
|
526
|
+
try {
|
|
527
|
+
const candidate = JSON5.parse(raw);
|
|
528
|
+
if (!isRecord(candidate)) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
parsed = candidate;
|
|
532
|
+
} catch {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const agents = isRecord(parsed.agents) ? parsed.agents : {};
|
|
537
|
+
const defaults = isRecord(agents.defaults) ? agents.defaults : {};
|
|
538
|
+
const nextList = Array.isArray(agents.list) ? [...agents.list] : [];
|
|
539
|
+
const rootTools = isRecord(parsed.tools) ? parsed.tools : {};
|
|
540
|
+
const rootExec = isRecord(rootTools.exec) ? rootTools.exec : {};
|
|
541
|
+
const plugins = isRecord(parsed.plugins) ? parsed.plugins : {};
|
|
542
|
+
const entries = isRecord(plugins.entries) ? plugins.entries : {};
|
|
543
|
+
const teamclawPluginEntry = isRecord(entries[TEAMCLAW_AGENT_ID]) ? entries[TEAMCLAW_AGENT_ID] : {};
|
|
544
|
+
const existingPluginConfig = isRecord(teamclawPluginEntry.config) ? teamclawPluginEntry.config : {};
|
|
545
|
+
const teamclawWorkspaceDir = path.join(resolveDefaultOpenClawStateDir(), `workspace-${TEAMCLAW_AGENT_ID}`);
|
|
546
|
+
const teamclawAgentDir = path.join(resolveDefaultOpenClawStateDir(), "agents", TEAMCLAW_AGENT_ID, "agent");
|
|
547
|
+
const existingIndex = nextList.findIndex((entry) => isRecord(entry) && normalizeAgentId(entry.id) === TEAMCLAW_AGENT_ID);
|
|
548
|
+
const existingEntry = existingIndex >= 0 && isRecord(nextList[existingIndex]) ? nextList[existingIndex] : {};
|
|
549
|
+
const existingEntryTools = isRecord(existingEntry.tools) ? existingEntry.tools : {};
|
|
550
|
+
const existingEntryExec = isRecord(existingEntryTools.exec) ? existingEntryTools.exec : {};
|
|
551
|
+
const nextEntryExec: Record<string, unknown> = {
|
|
552
|
+
...cloneJsonValue(rootExec),
|
|
553
|
+
...cloneJsonValue(existingEntryExec),
|
|
554
|
+
};
|
|
555
|
+
if (typeof nextEntryExec.security !== "string" || !nextEntryExec.security.trim()) {
|
|
556
|
+
nextEntryExec.security = typeof rootExec.security === "string" && rootExec.security.trim()
|
|
557
|
+
? rootExec.security.trim()
|
|
558
|
+
: TEAMCLAW_RECOMMENDED_EXEC_SECURITY;
|
|
559
|
+
}
|
|
560
|
+
if (typeof nextEntryExec.ask !== "string" || !nextEntryExec.ask.trim()) {
|
|
561
|
+
nextEntryExec.ask = typeof rootExec.ask === "string" && rootExec.ask.trim()
|
|
562
|
+
? rootExec.ask.trim()
|
|
563
|
+
: TEAMCLAW_RECOMMENDED_EXEC_ASK;
|
|
564
|
+
}
|
|
565
|
+
const nextEntry: Record<string, unknown> = {
|
|
566
|
+
...existingEntry,
|
|
567
|
+
id: TEAMCLAW_AGENT_ID,
|
|
568
|
+
workspace: typeof existingEntry.workspace === "string" && existingEntry.workspace.trim()
|
|
569
|
+
? existingEntry.workspace
|
|
570
|
+
: teamclawWorkspaceDir,
|
|
571
|
+
agentDir: typeof existingEntry.agentDir === "string" && existingEntry.agentDir.trim()
|
|
572
|
+
? existingEntry.agentDir
|
|
573
|
+
: teamclawAgentDir,
|
|
574
|
+
tools: {
|
|
575
|
+
...cloneJsonValue(existingEntryTools),
|
|
576
|
+
exec: nextEntryExec,
|
|
577
|
+
},
|
|
578
|
+
};
|
|
579
|
+
if (nextEntry.model == null && defaults.model != null) {
|
|
580
|
+
nextEntry.model = cloneJsonValue(defaults.model);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const nextPluginConfig: Record<string, unknown> = {
|
|
584
|
+
...existingPluginConfig,
|
|
585
|
+
};
|
|
586
|
+
if (
|
|
587
|
+
nextPluginConfig.mode === "controller"
|
|
588
|
+
&& nextPluginConfig.processModel === "multi"
|
|
589
|
+
&& nextPluginConfig.workerProvisioningType === "none"
|
|
590
|
+
&& nextPluginConfig.workerProvisioningDisabled !== true
|
|
591
|
+
) {
|
|
592
|
+
nextPluginConfig.workerProvisioningType = "process";
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const entryChanged = JSON.stringify(existingEntry) !== JSON.stringify(nextEntry);
|
|
596
|
+
const pluginConfigChanged = JSON.stringify(existingPluginConfig) !== JSON.stringify(nextPluginConfig);
|
|
597
|
+
if (!entryChanged && !pluginConfigChanged) {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (existingIndex >= 0) {
|
|
602
|
+
nextList[existingIndex] = nextEntry;
|
|
603
|
+
} else {
|
|
604
|
+
nextList.push(nextEntry);
|
|
605
|
+
}
|
|
606
|
+
agents.list = nextList;
|
|
607
|
+
parsed.agents = agents;
|
|
608
|
+
if (pluginConfigChanged) {
|
|
609
|
+
teamclawPluginEntry.config = nextPluginConfig;
|
|
610
|
+
entries[TEAMCLAW_AGENT_ID] = teamclawPluginEntry;
|
|
611
|
+
plugins.entries = entries;
|
|
612
|
+
parsed.plugins = plugins;
|
|
613
|
+
}
|
|
614
|
+
await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
|
|
615
|
+
logger.info(`TeamClaw: bootstrapped dedicated agent config into ${configPath}`);
|
|
616
|
+
}
|
|
617
|
+
|
|
151
618
|
async function ensureFileIfMissing(filePath: string, content: string): Promise<void> {
|
|
152
619
|
try {
|
|
153
620
|
await fs.access(filePath);
|