doer-agent 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-codex-auth-rpc.js +322 -0
- package/dist/agent-codex-cli.js +210 -0
- package/dist/agent-fs-rpc.js +405 -0
- package/dist/agent-git-rpc.js +299 -0
- package/dist/agent-jetstream.js +120 -0
- package/dist/agent-run-execution.js +39 -0
- package/dist/agent-run-lifecycle.js +67 -0
- package/dist/agent-run-rpc.js +93 -0
- package/dist/agent-run-state.js +229 -0
- package/dist/agent-runtime-env.js +147 -0
- package/dist/agent-runtime-io.js +112 -0
- package/dist/agent-runtime-utils.js +253 -0
- package/dist/agent-session-loop.js +53 -0
- package/dist/agent-session-rpc.js +867 -0
- package/dist/agent-settings-rpc.js +75 -0
- package/dist/agent-settings.js +397 -0
- package/dist/agent-skill-rpc.js +164 -0
- package/dist/agent-task-execution.js +275 -0
- package/dist/agent.js +376 -4275
- package/package.json +1 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { StringCodec } from "nats";
|
|
3
|
+
import { normalizeAgentSettingsConfig, normalizeAgentSettingsPatch, readAgentSettingsConfig, resolveAgentSettingsFilePath, toAgentSettingsPublic, writeAgentModelInstructions, writeAgentSettingsConfig, } from "./agent-settings.js";
|
|
4
|
+
const settingsRpcCodec = StringCodec();
|
|
5
|
+
function normalizeSettingsRpcRequest(args) {
|
|
6
|
+
const requestId = typeof args.request.requestId === "string" ? args.request.requestId.trim() : "";
|
|
7
|
+
const responseSubject = typeof args.request.responseSubject === "string" ? args.request.responseSubject.trim() : "";
|
|
8
|
+
const requestAgentId = typeof args.request.agentId === "string" ? args.request.agentId.trim() : "";
|
|
9
|
+
const action = args.request.action === "update" ? "update" : "get";
|
|
10
|
+
if (!requestId || !responseSubject || !requestAgentId || requestAgentId !== args.agentId) {
|
|
11
|
+
throw new Error("invalid settings rpc request");
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
requestId,
|
|
15
|
+
responseSubject,
|
|
16
|
+
action,
|
|
17
|
+
patch: normalizeAgentSettingsPatch(args.request.patch),
|
|
18
|
+
defaults: args.request.defaults && typeof args.request.defaults === "object" && !Array.isArray(args.request.defaults)
|
|
19
|
+
? normalizeAgentSettingsConfig(args.request.defaults)
|
|
20
|
+
: null,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function publishSettingsRpcResponse(args) {
|
|
24
|
+
args.nc.publish(args.responseSubject, settingsRpcCodec.encode(JSON.stringify(args.payload)));
|
|
25
|
+
}
|
|
26
|
+
export async function handleSettingsRpcMessage(args) {
|
|
27
|
+
let requestId = "unknown";
|
|
28
|
+
let responseSubject = "";
|
|
29
|
+
try {
|
|
30
|
+
const payload = JSON.parse(settingsRpcCodec.decode(args.msg.data));
|
|
31
|
+
const request = normalizeSettingsRpcRequest({ request: payload, agentId: args.agentId });
|
|
32
|
+
requestId = request.requestId;
|
|
33
|
+
responseSubject = request.responseSubject;
|
|
34
|
+
const existing = await readAgentSettingsConfig({ workspaceRoot: args.workspaceRoot, defaults: request.defaults });
|
|
35
|
+
const next = request.action === "update" ? normalizeAgentSettingsConfig(request.patch, existing) : existing;
|
|
36
|
+
if (request.action === "update") {
|
|
37
|
+
await writeAgentSettingsConfig({ workspaceRoot: args.workspaceRoot, config: next });
|
|
38
|
+
const customInstructions = typeof request.patch.customInstructions === "string"
|
|
39
|
+
? request.patch.customInstructions
|
|
40
|
+
: request.patch.customInstructions === null
|
|
41
|
+
? null
|
|
42
|
+
: undefined;
|
|
43
|
+
if (customInstructions !== undefined) {
|
|
44
|
+
await writeAgentModelInstructions({ workspaceRoot: args.workspaceRoot, value: customInstructions });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else if (request.defaults) {
|
|
48
|
+
const filePath = resolveAgentSettingsFilePath(args.workspaceRoot);
|
|
49
|
+
const raw = await readFile(filePath, "utf8").catch(() => "");
|
|
50
|
+
if (!raw.trim()) {
|
|
51
|
+
await writeAgentSettingsConfig({ workspaceRoot: args.workspaceRoot, config: next });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
publishSettingsRpcResponse({
|
|
55
|
+
nc: args.nc,
|
|
56
|
+
responseSubject,
|
|
57
|
+
payload: {
|
|
58
|
+
requestId,
|
|
59
|
+
ok: true,
|
|
60
|
+
settings: await toAgentSettingsPublic({ workspaceRoot: args.workspaceRoot, config: next }),
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66
|
+
if (responseSubject) {
|
|
67
|
+
publishSettingsRpcResponse({
|
|
68
|
+
nc: args.nc,
|
|
69
|
+
responseSubject,
|
|
70
|
+
payload: { requestId, ok: false, error: message },
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
args.onError(`settings rpc failed requestId=${requestId} error=${message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
function resolveAgentSettingsDir(workspaceRoot) {
|
|
4
|
+
return path.join(workspaceRoot, ".doer-agent");
|
|
5
|
+
}
|
|
6
|
+
export function resolveAgentSettingsFilePath(workspaceRoot) {
|
|
7
|
+
return path.join(resolveAgentSettingsDir(workspaceRoot), "config.json");
|
|
8
|
+
}
|
|
9
|
+
export function resolveAgentModelInstructionsFilePath(workspaceRoot) {
|
|
10
|
+
return path.join(resolveAgentSettingsDir(workspaceRoot), "model-instructions.md");
|
|
11
|
+
}
|
|
12
|
+
export function createDefaultAgentSettingsConfig() {
|
|
13
|
+
return {
|
|
14
|
+
general: {
|
|
15
|
+
personality: "pragmatic",
|
|
16
|
+
},
|
|
17
|
+
codex: {
|
|
18
|
+
model: "gpt-5.4",
|
|
19
|
+
authMode: "api_key",
|
|
20
|
+
},
|
|
21
|
+
realtime: {
|
|
22
|
+
model: process.env.OPENAI_REALTIME_MODEL?.trim() || "gpt-realtime",
|
|
23
|
+
voice: process.env.OPENAI_REALTIME_VOICE?.trim() || "alloy",
|
|
24
|
+
wakeName: null,
|
|
25
|
+
requireWakeName: true,
|
|
26
|
+
apiKey: null,
|
|
27
|
+
},
|
|
28
|
+
git: {
|
|
29
|
+
enabled: true,
|
|
30
|
+
name: null,
|
|
31
|
+
email: null,
|
|
32
|
+
authMode: "none",
|
|
33
|
+
oauthToken: null,
|
|
34
|
+
oauthLogin: null,
|
|
35
|
+
oauthScope: null,
|
|
36
|
+
},
|
|
37
|
+
aws: {
|
|
38
|
+
enabled: true,
|
|
39
|
+
accessKeyId: null,
|
|
40
|
+
defaultRegion: null,
|
|
41
|
+
secretAccessKey: null,
|
|
42
|
+
sessionToken: null,
|
|
43
|
+
},
|
|
44
|
+
jira: {
|
|
45
|
+
baseUrl: null,
|
|
46
|
+
email: null,
|
|
47
|
+
enabled: false,
|
|
48
|
+
apiToken: null,
|
|
49
|
+
},
|
|
50
|
+
notion: {
|
|
51
|
+
baseUrl: "https://api.notion.com",
|
|
52
|
+
version: "2022-06-28",
|
|
53
|
+
enabled: false,
|
|
54
|
+
apiToken: null,
|
|
55
|
+
},
|
|
56
|
+
slack: {
|
|
57
|
+
baseUrl: "https://slack.com/api",
|
|
58
|
+
enabled: false,
|
|
59
|
+
botToken: null,
|
|
60
|
+
},
|
|
61
|
+
figma: {
|
|
62
|
+
baseUrl: "https://api.figma.com",
|
|
63
|
+
enabled: false,
|
|
64
|
+
apiToken: null,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function normalizeNullableString(value) {
|
|
69
|
+
if (value === null) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (typeof value !== "string") {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const trimmed = value.trim();
|
|
76
|
+
return trimmed ? trimmed : null;
|
|
77
|
+
}
|
|
78
|
+
function normalizeCodexPersonality(value, fallback) {
|
|
79
|
+
return value === "friendly" || value === "pragmatic" ? value : fallback;
|
|
80
|
+
}
|
|
81
|
+
export function normalizeAgentSettingsConfig(value, fallback) {
|
|
82
|
+
const base = fallback ?? createDefaultAgentSettingsConfig();
|
|
83
|
+
const raw = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
84
|
+
const general = raw.general && typeof raw.general === "object" ? raw.general : {};
|
|
85
|
+
const codex = raw.codex && typeof raw.codex === "object" ? raw.codex : {};
|
|
86
|
+
const realtime = raw.realtime && typeof raw.realtime === "object" ? raw.realtime : {};
|
|
87
|
+
const git = raw.git && typeof raw.git === "object" ? raw.git : {};
|
|
88
|
+
const aws = raw.aws && typeof raw.aws === "object" ? raw.aws : {};
|
|
89
|
+
const jira = raw.jira && typeof raw.jira === "object" ? raw.jira : {};
|
|
90
|
+
const notion = raw.notion && typeof raw.notion === "object" ? raw.notion : {};
|
|
91
|
+
const slack = raw.slack && typeof raw.slack === "object" ? raw.slack : {};
|
|
92
|
+
const figma = raw.figma && typeof raw.figma === "object" ? raw.figma : {};
|
|
93
|
+
return {
|
|
94
|
+
general: {
|
|
95
|
+
personality: normalizeCodexPersonality(general.personality, base.general.personality),
|
|
96
|
+
},
|
|
97
|
+
codex: {
|
|
98
|
+
model: typeof codex.model === "string" && codex.model.trim() ? codex.model.trim() : base.codex.model,
|
|
99
|
+
authMode: codex.authMode === "oauth" ? "oauth" : codex.authMode === "api_key" ? "api_key" : base.codex.authMode,
|
|
100
|
+
},
|
|
101
|
+
realtime: {
|
|
102
|
+
model: typeof realtime.model === "string" && realtime.model.trim() ? realtime.model.trim() : base.realtime.model,
|
|
103
|
+
voice: typeof realtime.voice === "string" && realtime.voice.trim() ? realtime.voice.trim() : base.realtime.voice,
|
|
104
|
+
wakeName: realtime.wakeName === null ? null : normalizeNullableString(realtime.wakeName) ?? base.realtime.wakeName,
|
|
105
|
+
requireWakeName: typeof realtime.requireWakeName === "boolean" ? realtime.requireWakeName : base.realtime.requireWakeName,
|
|
106
|
+
apiKey: realtime.apiKey === null ? null : normalizeNullableString(realtime.apiKey) ?? base.realtime.apiKey,
|
|
107
|
+
},
|
|
108
|
+
git: {
|
|
109
|
+
enabled: typeof git.enabled === "boolean" ? git.enabled : base.git.enabled,
|
|
110
|
+
name: git.name === null ? null : normalizeNullableString(git.name) ?? base.git.name,
|
|
111
|
+
email: git.email === null ? null : normalizeNullableString(git.email) ?? base.git.email,
|
|
112
|
+
authMode: git.authMode === "oauth_app" ? "oauth_app" : git.authMode === "none" ? "none" : base.git.authMode,
|
|
113
|
+
oauthToken: git.oauthToken === null ? null : normalizeNullableString(git.oauthToken) ?? base.git.oauthToken,
|
|
114
|
+
oauthLogin: git.oauthLogin === null ? null : normalizeNullableString(git.oauthLogin) ?? base.git.oauthLogin,
|
|
115
|
+
oauthScope: git.oauthScope === null ? null : normalizeNullableString(git.oauthScope) ?? base.git.oauthScope,
|
|
116
|
+
},
|
|
117
|
+
aws: {
|
|
118
|
+
enabled: typeof aws.enabled === "boolean" ? aws.enabled : base.aws.enabled,
|
|
119
|
+
accessKeyId: aws.accessKeyId === null ? null : normalizeNullableString(aws.accessKeyId) ?? base.aws.accessKeyId,
|
|
120
|
+
defaultRegion: aws.defaultRegion === null ? null : normalizeNullableString(aws.defaultRegion) ?? base.aws.defaultRegion,
|
|
121
|
+
secretAccessKey: aws.secretAccessKey === null
|
|
122
|
+
? null
|
|
123
|
+
: normalizeNullableString(aws.secretAccessKey) ?? base.aws.secretAccessKey,
|
|
124
|
+
sessionToken: aws.sessionToken === null ? null : normalizeNullableString(aws.sessionToken) ?? base.aws.sessionToken,
|
|
125
|
+
},
|
|
126
|
+
jira: {
|
|
127
|
+
baseUrl: jira.baseUrl === null ? null : normalizeNullableString(jira.baseUrl) ?? base.jira.baseUrl,
|
|
128
|
+
email: jira.email === null ? null : normalizeNullableString(jira.email) ?? base.jira.email,
|
|
129
|
+
enabled: typeof jira.enabled === "boolean" ? jira.enabled : base.jira.enabled,
|
|
130
|
+
apiToken: jira.apiToken === null ? null : normalizeNullableString(jira.apiToken) ?? base.jira.apiToken,
|
|
131
|
+
},
|
|
132
|
+
notion: {
|
|
133
|
+
baseUrl: notion.baseUrl === null ? null : normalizeNullableString(notion.baseUrl) ?? base.notion.baseUrl,
|
|
134
|
+
version: notion.version === null ? null : normalizeNullableString(notion.version) ?? base.notion.version,
|
|
135
|
+
enabled: typeof notion.enabled === "boolean" ? notion.enabled : base.notion.enabled,
|
|
136
|
+
apiToken: notion.apiToken === null ? null : normalizeNullableString(notion.apiToken) ?? base.notion.apiToken,
|
|
137
|
+
},
|
|
138
|
+
slack: {
|
|
139
|
+
baseUrl: slack.baseUrl === null ? null : normalizeNullableString(slack.baseUrl) ?? base.slack.baseUrl,
|
|
140
|
+
enabled: typeof slack.enabled === "boolean" ? slack.enabled : base.slack.enabled,
|
|
141
|
+
botToken: slack.botToken === null ? null : normalizeNullableString(slack.botToken) ?? base.slack.botToken,
|
|
142
|
+
},
|
|
143
|
+
figma: {
|
|
144
|
+
baseUrl: figma.baseUrl === null ? null : normalizeNullableString(figma.baseUrl) ?? base.figma.baseUrl,
|
|
145
|
+
enabled: typeof figma.enabled === "boolean" ? figma.enabled : base.figma.enabled,
|
|
146
|
+
apiToken: figma.apiToken === null ? null : normalizeNullableString(figma.apiToken) ?? base.figma.apiToken,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
export async function readAgentSettingsConfig(args) {
|
|
151
|
+
const fallback = normalizeAgentSettingsConfig(args.defaults ?? null);
|
|
152
|
+
const filePath = resolveAgentSettingsFilePath(args.workspaceRoot);
|
|
153
|
+
const raw = await readFile(filePath, "utf8").catch(() => "");
|
|
154
|
+
if (!raw.trim()) {
|
|
155
|
+
return fallback;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
return normalizeAgentSettingsConfig(JSON.parse(raw), fallback);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return fallback;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
export async function writeAgentSettingsConfig(args) {
|
|
165
|
+
const dir = resolveAgentSettingsDir(args.workspaceRoot);
|
|
166
|
+
await mkdir(dir, { recursive: true });
|
|
167
|
+
await writeFile(resolveAgentSettingsFilePath(args.workspaceRoot), `${JSON.stringify(args.config, null, 2)}\n`, "utf8");
|
|
168
|
+
}
|
|
169
|
+
export async function readAgentModelInstructions(workspaceRoot) {
|
|
170
|
+
const raw = await readFile(resolveAgentModelInstructionsFilePath(workspaceRoot), "utf8").catch(() => "");
|
|
171
|
+
return raw.trim() ? raw : null;
|
|
172
|
+
}
|
|
173
|
+
export async function writeAgentModelInstructions(args) {
|
|
174
|
+
const filePath = resolveAgentModelInstructionsFilePath(args.workspaceRoot);
|
|
175
|
+
const nextValue = typeof args.value === "string" ? args.value.trim() : "";
|
|
176
|
+
if (!nextValue) {
|
|
177
|
+
await unlink(filePath).catch(() => undefined);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
await mkdir(resolveAgentSettingsDir(args.workspaceRoot), { recursive: true });
|
|
181
|
+
await writeFile(filePath, args.value ?? "", "utf8");
|
|
182
|
+
}
|
|
183
|
+
function maskSecretPreview(secret) {
|
|
184
|
+
if (secret.length <= 6) {
|
|
185
|
+
return `${secret.slice(0, 1)}***${secret.slice(-1)}`;
|
|
186
|
+
}
|
|
187
|
+
return `${secret.slice(0, 4)}...${secret.slice(-4)}`;
|
|
188
|
+
}
|
|
189
|
+
function toMaskedSecret(value) {
|
|
190
|
+
if (!value) {
|
|
191
|
+
return { has: false, masked: null, length: null };
|
|
192
|
+
}
|
|
193
|
+
return { has: true, masked: maskSecretPreview(value), length: value.length };
|
|
194
|
+
}
|
|
195
|
+
export async function toAgentSettingsPublic(args) {
|
|
196
|
+
const realtimeKey = toMaskedSecret(args.config.realtime.apiKey);
|
|
197
|
+
const gitOauth = toMaskedSecret(args.config.git.oauthToken);
|
|
198
|
+
const awsSecret = toMaskedSecret(args.config.aws.secretAccessKey);
|
|
199
|
+
const awsSession = toMaskedSecret(args.config.aws.sessionToken);
|
|
200
|
+
const jiraToken = toMaskedSecret(args.config.jira.apiToken);
|
|
201
|
+
const notionToken = toMaskedSecret(args.config.notion.apiToken);
|
|
202
|
+
const slackToken = toMaskedSecret(args.config.slack.botToken);
|
|
203
|
+
const figmaToken = toMaskedSecret(args.config.figma.apiToken);
|
|
204
|
+
const customInstructions = await readAgentModelInstructions(args.workspaceRoot);
|
|
205
|
+
return {
|
|
206
|
+
general: {
|
|
207
|
+
personality: args.config.general.personality,
|
|
208
|
+
customInstructions,
|
|
209
|
+
},
|
|
210
|
+
codex: {
|
|
211
|
+
model: args.config.codex.model,
|
|
212
|
+
authMode: args.config.codex.authMode,
|
|
213
|
+
hasApiKey: false,
|
|
214
|
+
apiKeyMasked: null,
|
|
215
|
+
apiKeyLength: null,
|
|
216
|
+
},
|
|
217
|
+
realtime: {
|
|
218
|
+
model: args.config.realtime.model,
|
|
219
|
+
voice: args.config.realtime.voice,
|
|
220
|
+
wakeName: args.config.realtime.wakeName,
|
|
221
|
+
requireWakeName: args.config.realtime.requireWakeName,
|
|
222
|
+
hasApiKey: realtimeKey.has,
|
|
223
|
+
apiKeyMasked: realtimeKey.masked,
|
|
224
|
+
apiKeyLength: realtimeKey.length,
|
|
225
|
+
},
|
|
226
|
+
git: {
|
|
227
|
+
enabled: args.config.git.enabled,
|
|
228
|
+
name: args.config.git.name,
|
|
229
|
+
email: args.config.git.email,
|
|
230
|
+
authMode: args.config.git.authMode,
|
|
231
|
+
hasOauthToken: gitOauth.has,
|
|
232
|
+
oauthTokenMasked: gitOauth.masked,
|
|
233
|
+
oauthTokenLength: gitOauth.length,
|
|
234
|
+
oauthLogin: args.config.git.oauthLogin,
|
|
235
|
+
oauthScope: args.config.git.oauthScope,
|
|
236
|
+
},
|
|
237
|
+
aws: {
|
|
238
|
+
enabled: args.config.aws.enabled,
|
|
239
|
+
accessKeyId: args.config.aws.accessKeyId,
|
|
240
|
+
defaultRegion: args.config.aws.defaultRegion,
|
|
241
|
+
hasSecretAccessKey: awsSecret.has,
|
|
242
|
+
secretAccessKeyMasked: awsSecret.masked,
|
|
243
|
+
secretAccessKeyLength: awsSecret.length,
|
|
244
|
+
hasSessionToken: awsSession.has,
|
|
245
|
+
sessionTokenMasked: awsSession.masked,
|
|
246
|
+
sessionTokenLength: awsSession.length,
|
|
247
|
+
},
|
|
248
|
+
jira: {
|
|
249
|
+
baseUrl: args.config.jira.baseUrl,
|
|
250
|
+
email: args.config.jira.email,
|
|
251
|
+
enabled: args.config.jira.enabled,
|
|
252
|
+
hasApiToken: jiraToken.has,
|
|
253
|
+
apiTokenMasked: jiraToken.masked,
|
|
254
|
+
apiTokenLength: jiraToken.length,
|
|
255
|
+
},
|
|
256
|
+
notion: {
|
|
257
|
+
baseUrl: args.config.notion.baseUrl,
|
|
258
|
+
version: args.config.notion.version,
|
|
259
|
+
enabled: args.config.notion.enabled,
|
|
260
|
+
hasApiToken: notionToken.has,
|
|
261
|
+
apiTokenMasked: notionToken.masked,
|
|
262
|
+
apiTokenLength: notionToken.length,
|
|
263
|
+
},
|
|
264
|
+
slack: {
|
|
265
|
+
baseUrl: args.config.slack.baseUrl,
|
|
266
|
+
enabled: args.config.slack.enabled,
|
|
267
|
+
hasBotToken: slackToken.has,
|
|
268
|
+
botTokenMasked: slackToken.masked,
|
|
269
|
+
botTokenLength: slackToken.length,
|
|
270
|
+
},
|
|
271
|
+
figma: {
|
|
272
|
+
baseUrl: args.config.figma.baseUrl,
|
|
273
|
+
enabled: args.config.figma.enabled,
|
|
274
|
+
hasApiToken: figmaToken.has,
|
|
275
|
+
apiTokenMasked: figmaToken.masked,
|
|
276
|
+
apiTokenLength: figmaToken.length,
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
export function normalizeAgentSettingsPatch(value) {
|
|
281
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
282
|
+
return {};
|
|
283
|
+
}
|
|
284
|
+
const raw = value;
|
|
285
|
+
const patch = { ...raw };
|
|
286
|
+
const assignNested = (section, key, nextValue) => {
|
|
287
|
+
const current = patch[section] && typeof patch[section] === "object" && !Array.isArray(patch[section])
|
|
288
|
+
? { ...patch[section] }
|
|
289
|
+
: {};
|
|
290
|
+
current[key] = nextValue;
|
|
291
|
+
patch[section] = current;
|
|
292
|
+
};
|
|
293
|
+
const move = (flatKey, section, key) => {
|
|
294
|
+
if (!(flatKey in raw)) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
assignNested(section, key, raw[flatKey]);
|
|
298
|
+
delete patch[flatKey];
|
|
299
|
+
};
|
|
300
|
+
move("personality", "general", "personality");
|
|
301
|
+
move("codexModel", "codex", "model");
|
|
302
|
+
move("codexAuthMode", "codex", "authMode");
|
|
303
|
+
move("realtimeModel", "realtime", "model");
|
|
304
|
+
move("realtimeVoice", "realtime", "voice");
|
|
305
|
+
move("realtimeWakeName", "realtime", "wakeName");
|
|
306
|
+
move("realtimeRequireWakeName", "realtime", "requireWakeName");
|
|
307
|
+
move("realtimeApiKey", "realtime", "apiKey");
|
|
308
|
+
move("gitEnabled", "git", "enabled");
|
|
309
|
+
move("gitName", "git", "name");
|
|
310
|
+
move("gitEmail", "git", "email");
|
|
311
|
+
move("gitAuthMode", "git", "authMode");
|
|
312
|
+
move("gitOauthToken", "git", "oauthToken");
|
|
313
|
+
move("gitOauthLogin", "git", "oauthLogin");
|
|
314
|
+
move("gitOauthScope", "git", "oauthScope");
|
|
315
|
+
move("awsEnabled", "aws", "enabled");
|
|
316
|
+
move("awsAccessKeyId", "aws", "accessKeyId");
|
|
317
|
+
move("awsDefaultRegion", "aws", "defaultRegion");
|
|
318
|
+
move("awsSecretAccessKey", "aws", "secretAccessKey");
|
|
319
|
+
move("awsSessionToken", "aws", "sessionToken");
|
|
320
|
+
move("jiraBaseUrl", "jira", "baseUrl");
|
|
321
|
+
move("jiraEmail", "jira", "email");
|
|
322
|
+
move("jiraEnabled", "jira", "enabled");
|
|
323
|
+
move("jiraApiToken", "jira", "apiToken");
|
|
324
|
+
move("notionBaseUrl", "notion", "baseUrl");
|
|
325
|
+
move("notionVersion", "notion", "version");
|
|
326
|
+
move("notionEnabled", "notion", "enabled");
|
|
327
|
+
move("notionApiToken", "notion", "apiToken");
|
|
328
|
+
move("slackBaseUrl", "slack", "baseUrl");
|
|
329
|
+
move("slackEnabled", "slack", "enabled");
|
|
330
|
+
move("slackBotToken", "slack", "botToken");
|
|
331
|
+
move("figmaBaseUrl", "figma", "baseUrl");
|
|
332
|
+
move("figmaEnabled", "figma", "enabled");
|
|
333
|
+
move("figmaApiToken", "figma", "apiToken");
|
|
334
|
+
return patch;
|
|
335
|
+
}
|
|
336
|
+
export function buildAgentSettingsEnvPatch(config) {
|
|
337
|
+
const envPatch = {};
|
|
338
|
+
if (config.git.enabled) {
|
|
339
|
+
if (config.git.name)
|
|
340
|
+
envPatch.GIT_AUTHOR_NAME = config.git.name;
|
|
341
|
+
if (config.git.name)
|
|
342
|
+
envPatch.GIT_COMMITTER_NAME = config.git.name;
|
|
343
|
+
if (config.git.email)
|
|
344
|
+
envPatch.GIT_AUTHOR_EMAIL = config.git.email;
|
|
345
|
+
if (config.git.email)
|
|
346
|
+
envPatch.GIT_COMMITTER_EMAIL = config.git.email;
|
|
347
|
+
if (config.git.oauthToken)
|
|
348
|
+
envPatch.GITHUB_TOKEN = config.git.oauthToken;
|
|
349
|
+
if (config.git.oauthToken)
|
|
350
|
+
envPatch.GH_TOKEN = config.git.oauthToken;
|
|
351
|
+
if (config.git.oauthLogin)
|
|
352
|
+
envPatch.DOER_GIT_OAUTH_LOGIN = config.git.oauthLogin;
|
|
353
|
+
if (config.git.oauthScope)
|
|
354
|
+
envPatch.DOER_GIT_OAUTH_SCOPE = config.git.oauthScope;
|
|
355
|
+
}
|
|
356
|
+
if (config.aws.enabled) {
|
|
357
|
+
if (config.aws.accessKeyId)
|
|
358
|
+
envPatch.AWS_ACCESS_KEY_ID = config.aws.accessKeyId;
|
|
359
|
+
if (config.aws.defaultRegion)
|
|
360
|
+
envPatch.AWS_DEFAULT_REGION = config.aws.defaultRegion;
|
|
361
|
+
if (config.aws.defaultRegion)
|
|
362
|
+
envPatch.AWS_REGION = config.aws.defaultRegion;
|
|
363
|
+
if (config.aws.secretAccessKey)
|
|
364
|
+
envPatch.AWS_SECRET_ACCESS_KEY = config.aws.secretAccessKey;
|
|
365
|
+
if (config.aws.sessionToken)
|
|
366
|
+
envPatch.AWS_SESSION_TOKEN = config.aws.sessionToken;
|
|
367
|
+
}
|
|
368
|
+
if (config.jira.enabled) {
|
|
369
|
+
if (config.jira.baseUrl)
|
|
370
|
+
envPatch.JIRA_BASE_URL = config.jira.baseUrl;
|
|
371
|
+
if (config.jira.email)
|
|
372
|
+
envPatch.JIRA_EMAIL = config.jira.email;
|
|
373
|
+
if (config.jira.apiToken)
|
|
374
|
+
envPatch.JIRA_API_TOKEN = config.jira.apiToken;
|
|
375
|
+
}
|
|
376
|
+
if (config.notion.enabled) {
|
|
377
|
+
if (config.notion.baseUrl)
|
|
378
|
+
envPatch.NOTION_BASE_URL = config.notion.baseUrl;
|
|
379
|
+
if (config.notion.version)
|
|
380
|
+
envPatch.NOTION_VERSION = config.notion.version;
|
|
381
|
+
if (config.notion.apiToken)
|
|
382
|
+
envPatch.NOTION_API_TOKEN = config.notion.apiToken;
|
|
383
|
+
}
|
|
384
|
+
if (config.slack.enabled) {
|
|
385
|
+
if (config.slack.baseUrl)
|
|
386
|
+
envPatch.SLACK_BASE_URL = config.slack.baseUrl;
|
|
387
|
+
if (config.slack.botToken)
|
|
388
|
+
envPatch.SLACK_BOT_TOKEN = config.slack.botToken;
|
|
389
|
+
}
|
|
390
|
+
if (config.figma.enabled) {
|
|
391
|
+
if (config.figma.baseUrl)
|
|
392
|
+
envPatch.FIGMA_BASE_URL = config.figma.baseUrl;
|
|
393
|
+
if (config.figma.apiToken)
|
|
394
|
+
envPatch.FIGMA_API_TOKEN = config.figma.apiToken;
|
|
395
|
+
}
|
|
396
|
+
return envPatch;
|
|
397
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { mkdir, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { StringCodec } from "nats";
|
|
4
|
+
const skillRpcCodec = StringCodec();
|
|
5
|
+
function buildSkillGeneratorPrompt(userPrompt) {
|
|
6
|
+
return [
|
|
7
|
+
"Create a Codex skill from the user's description.",
|
|
8
|
+
"",
|
|
9
|
+
"Return JSON only with this shape:",
|
|
10
|
+
'{ "skillName": "kebab-or-dot-or-underscore-name", "skillMd": "full SKILL.md content" }',
|
|
11
|
+
"",
|
|
12
|
+
"Requirements:",
|
|
13
|
+
"- skillName must be lowercase ASCII and use only letters, numbers, dot, underscore, or dash.",
|
|
14
|
+
"- skillName must not start with a dot and must not be .system.",
|
|
15
|
+
"- skillMd must be a complete SKILL.md file.",
|
|
16
|
+
"- skillMd must start with YAML frontmatter containing name and description.",
|
|
17
|
+
"- Keep the skill concise and action-oriented.",
|
|
18
|
+
"- Include when to use the skill, the core workflow, and any important constraints.",
|
|
19
|
+
"- Do not add README, changelog, or any extra files.",
|
|
20
|
+
"",
|
|
21
|
+
"User request:",
|
|
22
|
+
userPrompt.trim(),
|
|
23
|
+
].join("\n");
|
|
24
|
+
}
|
|
25
|
+
function extractJsonObject(value) {
|
|
26
|
+
const trimmed = value.trim();
|
|
27
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
28
|
+
return trimmed;
|
|
29
|
+
}
|
|
30
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
31
|
+
if (fenced?.[1]) {
|
|
32
|
+
return fenced[1].trim();
|
|
33
|
+
}
|
|
34
|
+
const start = trimmed.indexOf("{");
|
|
35
|
+
const end = trimmed.lastIndexOf("}");
|
|
36
|
+
if (start >= 0 && end > start) {
|
|
37
|
+
return trimmed.slice(start, end + 1);
|
|
38
|
+
}
|
|
39
|
+
throw new Error("Codex did not return JSON");
|
|
40
|
+
}
|
|
41
|
+
function slugifySkillName(value) {
|
|
42
|
+
return value
|
|
43
|
+
.trim()
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
46
|
+
.replace(/^-+|-+$/g, "")
|
|
47
|
+
.replace(/-{2,}/g, "-");
|
|
48
|
+
}
|
|
49
|
+
function normalizeGeneratedSkill(value) {
|
|
50
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
51
|
+
throw new Error("Invalid generated skill payload");
|
|
52
|
+
}
|
|
53
|
+
const row = value;
|
|
54
|
+
const skillName = slugifySkillName(typeof row.skillName === "string" ? row.skillName : "");
|
|
55
|
+
const skillMd = typeof row.skillMd === "string" ? row.skillMd.trim() : "";
|
|
56
|
+
if (!skillName || skillName.startsWith(".") || skillName === ".system") {
|
|
57
|
+
throw new Error("Codex returned an invalid skill name");
|
|
58
|
+
}
|
|
59
|
+
if (!skillMd) {
|
|
60
|
+
throw new Error("Codex returned an empty SKILL.md");
|
|
61
|
+
}
|
|
62
|
+
if (!/^---\s*\n[\s\S]*?\n---\s*\n/m.test(skillMd)) {
|
|
63
|
+
throw new Error("Generated SKILL.md is missing YAML frontmatter");
|
|
64
|
+
}
|
|
65
|
+
if (!/\nname:\s*[^\n]+/i.test(skillMd) || !/\ndescription:\s*[^\n]+/i.test(skillMd)) {
|
|
66
|
+
throw new Error("Generated SKILL.md frontmatter is incomplete");
|
|
67
|
+
}
|
|
68
|
+
return { skillName, skillMd };
|
|
69
|
+
}
|
|
70
|
+
function buildSkillGeneratorCodexArgs(prompt, model) {
|
|
71
|
+
return ["--dangerously-bypass-approvals-and-sandbox", "--model", model, "exec", "--", prompt];
|
|
72
|
+
}
|
|
73
|
+
async function generateSkillViaCodex(args) {
|
|
74
|
+
const localAgentSettings = await args.readAgentSettingsConfig({ workspaceRoot: args.workspaceRoot });
|
|
75
|
+
const envPatch = args.buildAgentSettingsEnvPatch(localAgentSettings);
|
|
76
|
+
const prompt = buildSkillGeneratorPrompt(args.userPrompt);
|
|
77
|
+
const result = await args.runLocalCodexCli(buildSkillGeneratorCodexArgs(prompt, localAgentSettings.codex.model || "gpt-5.4"), 120_000, envPatch);
|
|
78
|
+
if (result.timedOut) {
|
|
79
|
+
throw new Error("Codex timed out while generating the skill");
|
|
80
|
+
}
|
|
81
|
+
if ((result.code ?? 1) !== 0) {
|
|
82
|
+
const details = args.stripAnsi(result.stderr || result.stdout).trim();
|
|
83
|
+
throw new Error(details || `Codex exited with code ${result.code ?? "null"}`);
|
|
84
|
+
}
|
|
85
|
+
const payload = JSON.parse(extractJsonObject(args.stripAnsi(result.stdout)));
|
|
86
|
+
const generated = normalizeGeneratedSkill(payload);
|
|
87
|
+
const skillPath = path.join(args.resolveCodexHomePath(), "skills", generated.skillName);
|
|
88
|
+
const skillFilePath = path.join(skillPath, "SKILL.md");
|
|
89
|
+
try {
|
|
90
|
+
await stat(skillPath);
|
|
91
|
+
throw new Error("A skill with that name already exists");
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (!(error instanceof Error) || !/ENOENT/i.test(error.message)) {
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
await mkdir(skillPath, { recursive: true });
|
|
99
|
+
await writeFile(skillFilePath, `${generated.skillMd}\n`, "utf8");
|
|
100
|
+
return {
|
|
101
|
+
skillName: generated.skillName,
|
|
102
|
+
skillPath: `.codex/skills/${generated.skillName}`,
|
|
103
|
+
skillFilePath: `.codex/skills/${generated.skillName}/SKILL.md`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
async function handleSkillRpcMessage(args) {
|
|
107
|
+
let payload = {};
|
|
108
|
+
try {
|
|
109
|
+
payload = JSON.parse(skillRpcCodec.decode(args.msg.data));
|
|
110
|
+
if (typeof payload.agentId === "string" && payload.agentId.trim() && payload.agentId !== args.agentId) {
|
|
111
|
+
throw new Error("agent id mismatch");
|
|
112
|
+
}
|
|
113
|
+
const prompt = typeof payload.prompt === "string" ? payload.prompt.trim() : "";
|
|
114
|
+
if (!prompt) {
|
|
115
|
+
throw new Error("prompt is required");
|
|
116
|
+
}
|
|
117
|
+
const result = await generateSkillViaCodex({
|
|
118
|
+
userPrompt: prompt,
|
|
119
|
+
workspaceRoot: args.workspaceRoot,
|
|
120
|
+
resolveCodexHomePath: args.resolveCodexHomePath,
|
|
121
|
+
readAgentSettingsConfig: args.readAgentSettingsConfig,
|
|
122
|
+
buildAgentSettingsEnvPatch: args.buildAgentSettingsEnvPatch,
|
|
123
|
+
runLocalCodexCli: args.runLocalCodexCli,
|
|
124
|
+
stripAnsi: args.stripAnsi,
|
|
125
|
+
});
|
|
126
|
+
args.msg.respond(skillRpcCodec.encode(JSON.stringify({
|
|
127
|
+
ok: true,
|
|
128
|
+
skillName: result.skillName,
|
|
129
|
+
skillPath: result.skillPath,
|
|
130
|
+
skillFilePath: result.skillFilePath,
|
|
131
|
+
})));
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
const message = error instanceof Error ? error.message : "unknown error";
|
|
135
|
+
args.msg.respond(skillRpcCodec.encode(JSON.stringify({
|
|
136
|
+
ok: false,
|
|
137
|
+
error: message,
|
|
138
|
+
})));
|
|
139
|
+
args.onError(`skill rpc failed error=${message}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export function subscribeToSkillRpc(args) {
|
|
143
|
+
args.nc.subscribe(args.subject, {
|
|
144
|
+
callback: (error, msg) => {
|
|
145
|
+
if (error) {
|
|
146
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
147
|
+
args.onError(`skill rpc subscription error: ${message}`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
void handleSkillRpcMessage({
|
|
151
|
+
msg,
|
|
152
|
+
agentId: args.agentId,
|
|
153
|
+
workspaceRoot: args.workspaceRoot,
|
|
154
|
+
resolveCodexHomePath: args.resolveCodexHomePath,
|
|
155
|
+
readAgentSettingsConfig: args.readAgentSettingsConfig,
|
|
156
|
+
buildAgentSettingsEnvPatch: args.buildAgentSettingsEnvPatch,
|
|
157
|
+
runLocalCodexCli: args.runLocalCodexCli,
|
|
158
|
+
stripAnsi: args.stripAnsi,
|
|
159
|
+
onError: args.onError,
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
args.onInfo(`skill rpc subscribed subject=${args.subject}`);
|
|
164
|
+
}
|