workspacecord 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +190 -0
- package/README.zh-CN.md +190 -0
- package/dist/bot-B5HN4ZW6.js +6710 -0
- package/dist/chunk-2LBNM64L.js +84 -0
- package/dist/chunk-K3NQKI34.js +10 -0
- package/dist/chunk-NIXZJTOZ.js +175 -0
- package/dist/chunk-OKI4UVGY.js +221 -0
- package/dist/chunk-TSBM3BNT.js +1224 -0
- package/dist/chunk-WE4X3JB3.js +130 -0
- package/dist/cli.js +71 -0
- package/dist/codex-launcher-IF2IPLBP.js +132 -0
- package/dist/codex-provider-7CI5W34X.js +304 -0
- package/dist/config-cli-F2B5SYHJ.js +120 -0
- package/dist/daemon-NW4WRMQK.js +252 -0
- package/dist/project-cli-FEMPZIRQ.js +121 -0
- package/dist/project-registry-DQT5ORUU.js +32 -0
- package/dist/setup-TKOVXSME.js +262 -0
- package/dist/thread-manager-5T46QTZF.js +78 -0
- package/dist/utils-72GMT2X5.js +36 -0
- package/package.json +79 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/utils.ts
|
|
4
|
+
import { resolve, isAbsolute } from "path";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
function sanitizeName(name) {
|
|
7
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 50) || "session";
|
|
8
|
+
}
|
|
9
|
+
function resolvePath(p) {
|
|
10
|
+
if (p.startsWith("~/") || p === "~") {
|
|
11
|
+
return p.replace("~", homedir());
|
|
12
|
+
}
|
|
13
|
+
return isAbsolute(p) ? p : resolve(process.cwd(), p);
|
|
14
|
+
}
|
|
15
|
+
function isPathAllowed(path, allowedPaths) {
|
|
16
|
+
if (allowedPaths.length === 0) return true;
|
|
17
|
+
const resolved = resolvePath(path);
|
|
18
|
+
return allowedPaths.some((allowed) => {
|
|
19
|
+
const resolvedAllowed = resolvePath(allowed);
|
|
20
|
+
return resolved === resolvedAllowed || resolved.startsWith(resolvedAllowed + "/");
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function projectNameFromChannel(channelName) {
|
|
24
|
+
return channelName;
|
|
25
|
+
}
|
|
26
|
+
function formatDuration(ms) {
|
|
27
|
+
const s = Math.floor(ms / 1e3);
|
|
28
|
+
if (s < 60) return `${s}s`;
|
|
29
|
+
const m = Math.floor(s / 60);
|
|
30
|
+
if (m < 60) return `${m}m ${s % 60}s`;
|
|
31
|
+
const h = Math.floor(m / 60);
|
|
32
|
+
return `${h}h ${m % 60}m`;
|
|
33
|
+
}
|
|
34
|
+
function formatRelative(ts) {
|
|
35
|
+
const diff = Date.now() - ts;
|
|
36
|
+
if (diff < 6e4) return "just now";
|
|
37
|
+
if (diff < 36e5) return `${Math.floor(diff / 6e4)}m ago`;
|
|
38
|
+
if (diff < 864e5) return `${Math.floor(diff / 36e5)}h ago`;
|
|
39
|
+
return `${Math.floor(diff / 864e5)}d ago`;
|
|
40
|
+
}
|
|
41
|
+
function truncate(s, max) {
|
|
42
|
+
if (s.length <= max) return s;
|
|
43
|
+
return s.slice(0, max - 1) + "\u2026";
|
|
44
|
+
}
|
|
45
|
+
function isUserAllowed(userId, allowedUsers, allowAll) {
|
|
46
|
+
if (allowAll) return true;
|
|
47
|
+
if (allowedUsers.length === 0) return true;
|
|
48
|
+
return allowedUsers.includes(userId);
|
|
49
|
+
}
|
|
50
|
+
var ABORT_PATTERNS = ["abort", "cancel", "interrupt", "killed", "signal"];
|
|
51
|
+
function isAbortError(err) {
|
|
52
|
+
if (err instanceof Error && err.name === "AbortError") return true;
|
|
53
|
+
const msg = (err.message || "").toLowerCase();
|
|
54
|
+
return ABORT_PATTERNS.some((p) => msg.includes(p));
|
|
55
|
+
}
|
|
56
|
+
function isAbortErrorMessage(messages) {
|
|
57
|
+
return messages.some((m) => ABORT_PATTERNS.some((p) => m.toLowerCase().includes(p)));
|
|
58
|
+
}
|
|
59
|
+
function detectNumberedOptions(text) {
|
|
60
|
+
const lines = text.trim().split("\n");
|
|
61
|
+
const options = [];
|
|
62
|
+
const optionRegex = /^\s*(\d+)[.)]\s+(.+)$/;
|
|
63
|
+
let firstOptionLine = -1;
|
|
64
|
+
let lastOptionLine = -1;
|
|
65
|
+
for (let i = 0; i < lines.length; i++) {
|
|
66
|
+
const match = lines[i].match(optionRegex);
|
|
67
|
+
if (match) {
|
|
68
|
+
if (firstOptionLine === -1) firstOptionLine = i;
|
|
69
|
+
lastOptionLine = i;
|
|
70
|
+
options.push(match[2].trim());
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (options.length < 2 || options.length > 6) return null;
|
|
74
|
+
if (options.some((o) => o.length > 80)) return null;
|
|
75
|
+
const linesAfter = lines.slice(lastOptionLine + 1).filter((l) => l.trim()).length;
|
|
76
|
+
if (linesAfter > 3) return null;
|
|
77
|
+
const preamble = lines.slice(0, firstOptionLine).join(" ").toLowerCase();
|
|
78
|
+
const hasQuestion = /\?\s*$/.test(preamble.trim()) || /\b(which|choose|select|pick|prefer|would you like|how would you|what approach|option)\b/.test(
|
|
79
|
+
preamble
|
|
80
|
+
);
|
|
81
|
+
return hasQuestion ? options : null;
|
|
82
|
+
}
|
|
83
|
+
function detectYesNoPrompt(text) {
|
|
84
|
+
const lower = text.toLowerCase();
|
|
85
|
+
return /\b(y\/n|yes\/no|confirm|proceed)\b/.test(lower) || /\?\s*$/.test(text.trim()) && /\b(should|would you|do you want|shall)\b/.test(lower);
|
|
86
|
+
}
|
|
87
|
+
function formatUptime(startTime) {
|
|
88
|
+
const ms = Date.now() - startTime;
|
|
89
|
+
const seconds = Math.floor(ms / 1e3);
|
|
90
|
+
const minutes = Math.floor(seconds / 60);
|
|
91
|
+
const hours = Math.floor(minutes / 60);
|
|
92
|
+
const days = Math.floor(hours / 24);
|
|
93
|
+
if (days > 0) return `${days}d ${hours % 24}h`;
|
|
94
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
|
95
|
+
if (minutes > 0) return `${minutes}m`;
|
|
96
|
+
return `${seconds}s`;
|
|
97
|
+
}
|
|
98
|
+
function splitMessage(text, max = 1900) {
|
|
99
|
+
if (text.length <= max) return [text];
|
|
100
|
+
const chunks = [];
|
|
101
|
+
let i = 0;
|
|
102
|
+
while (i < text.length) {
|
|
103
|
+
chunks.push(text.slice(i, i + max));
|
|
104
|
+
i += max;
|
|
105
|
+
}
|
|
106
|
+
return chunks;
|
|
107
|
+
}
|
|
108
|
+
function formatCost(usd) {
|
|
109
|
+
if (usd === 0) return "$0.00";
|
|
110
|
+
if (usd < 0.01) return `$${usd.toFixed(4)}`;
|
|
111
|
+
return `$${usd.toFixed(2)}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export {
|
|
115
|
+
sanitizeName,
|
|
116
|
+
resolvePath,
|
|
117
|
+
isPathAllowed,
|
|
118
|
+
projectNameFromChannel,
|
|
119
|
+
formatDuration,
|
|
120
|
+
formatRelative,
|
|
121
|
+
truncate,
|
|
122
|
+
isUserAllowed,
|
|
123
|
+
isAbortError,
|
|
124
|
+
isAbortErrorMessage,
|
|
125
|
+
detectNumberedOptions,
|
|
126
|
+
detectYesNoPrompt,
|
|
127
|
+
formatUptime,
|
|
128
|
+
splitMessage,
|
|
129
|
+
formatCost
|
|
130
|
+
};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
var command = process.argv[2];
|
|
5
|
+
switch (command) {
|
|
6
|
+
case "setup": {
|
|
7
|
+
const { handleConfig } = await import("./config-cli-F2B5SYHJ.js");
|
|
8
|
+
await handleConfig(["setup"]);
|
|
9
|
+
break;
|
|
10
|
+
}
|
|
11
|
+
case "config": {
|
|
12
|
+
const { handleConfig } = await import("./config-cli-F2B5SYHJ.js");
|
|
13
|
+
await handleConfig(process.argv.slice(3));
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
case "start":
|
|
17
|
+
case void 0: {
|
|
18
|
+
const { startBot } = await import("./bot-B5HN4ZW6.js");
|
|
19
|
+
console.log("workspacecord starting...");
|
|
20
|
+
await startBot();
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
case "project": {
|
|
24
|
+
const { handleProject } = await import("./project-cli-FEMPZIRQ.js");
|
|
25
|
+
await handleProject(process.argv.slice(3));
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
case "daemon": {
|
|
29
|
+
const { handleDaemon } = await import("./daemon-NW4WRMQK.js");
|
|
30
|
+
await handleDaemon(process.argv[3]);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
case "codex": {
|
|
34
|
+
const { handleCodexCommand } = await import("./codex-launcher-IF2IPLBP.js");
|
|
35
|
+
await handleCodexCommand(process.argv.slice(3));
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
case "help":
|
|
39
|
+
case "--help":
|
|
40
|
+
case "-h": {
|
|
41
|
+
console.log(`
|
|
42
|
+
\x1B[1mworkspacecord\x1B[0m \u2014 Discord bot for multi-agent coding sessions
|
|
43
|
+
|
|
44
|
+
\x1B[1mUsage:\x1B[0m
|
|
45
|
+
workspacecord Start the bot
|
|
46
|
+
workspacecord config setup Interactive configuration wizard
|
|
47
|
+
workspacecord config get <key> Read a config value
|
|
48
|
+
workspacecord config set <k> <v> Write a config value
|
|
49
|
+
workspacecord config list List all config values
|
|
50
|
+
workspacecord config path Show config file path
|
|
51
|
+
workspacecord project <subcommand> Manage mounted projects
|
|
52
|
+
workspacecord daemon Manage background service (install/uninstall/status)
|
|
53
|
+
workspacecord codex [options] Launch managed Codex session with remote approval
|
|
54
|
+
workspacecord help Show this help message
|
|
55
|
+
|
|
56
|
+
\x1B[1mQuick start:\x1B[0m
|
|
57
|
+
1. workspacecord config setup Configure Discord app, token, permissions
|
|
58
|
+
2. workspacecord project init Mount a local project
|
|
59
|
+
3. workspacecord Start the bot
|
|
60
|
+
4. /project setup project:<name> Bind a Discord category to the mounted project
|
|
61
|
+
5. /agent spawn label:<task> Create an agent session
|
|
62
|
+
|
|
63
|
+
\x1B[2mhttps://github.com/xuhongbo/WorkspaceCord\x1B[0m
|
|
64
|
+
`);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
default:
|
|
68
|
+
console.error(`Unknown command: ${command}`);
|
|
69
|
+
console.error("Run \x1B[36mworkspacecord help\x1B[0m for usage.");
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getConfigValue
|
|
4
|
+
} from "./chunk-OKI4UVGY.js";
|
|
5
|
+
import "./chunk-K3NQKI34.js";
|
|
6
|
+
|
|
7
|
+
// src/cli/codex-launcher.ts
|
|
8
|
+
import { spawn } from "child_process";
|
|
9
|
+
import { existsSync } from "fs";
|
|
10
|
+
async function launchManagedCodex(options = {}) {
|
|
11
|
+
const cwd = options.cwd || process.cwd();
|
|
12
|
+
const codexBin = resolveCodexPath();
|
|
13
|
+
if (!existsSync(cwd)) {
|
|
14
|
+
console.error(`\u274C \u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5B58\u5728: ${cwd}`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
const codexArgs = [];
|
|
18
|
+
if (options.model) {
|
|
19
|
+
codexArgs.push("--model", options.model);
|
|
20
|
+
}
|
|
21
|
+
if (options.sandboxMode) {
|
|
22
|
+
codexArgs.push("--sandbox-mode", options.sandboxMode);
|
|
23
|
+
}
|
|
24
|
+
if (options.approvalPolicy) {
|
|
25
|
+
codexArgs.push("--approval-policy", options.approvalPolicy);
|
|
26
|
+
}
|
|
27
|
+
const env = {
|
|
28
|
+
...process.env,
|
|
29
|
+
workspacecord_MANAGED: "1",
|
|
30
|
+
workspacecord_SESSION_CWD: cwd
|
|
31
|
+
};
|
|
32
|
+
if (options.args) {
|
|
33
|
+
codexArgs.push(...options.args);
|
|
34
|
+
}
|
|
35
|
+
console.log("\u{1F680} \u542F\u52A8\u53D7\u7BA1 Codex \u4F1A\u8BDD...");
|
|
36
|
+
console.log(`\u{1F4C1} \u5DE5\u4F5C\u76EE\u5F55: ${cwd}`);
|
|
37
|
+
console.log(`\u{1F527} \u53C2\u6570: ${codexArgs.join(" ")}`);
|
|
38
|
+
console.log("");
|
|
39
|
+
console.log("\u{1F4A1} \u6B64\u4F1A\u8BDD\u652F\u6301 Discord \u8FDC\u7A0B\u5BA1\u6279\u80FD\u529B");
|
|
40
|
+
console.log(" \u5728 Discord \u4E2D\u53EF\u4EE5\u8FDC\u7A0B\u5141\u8BB8/\u62D2\u7EDD\u64CD\u4F5C");
|
|
41
|
+
console.log("");
|
|
42
|
+
const codex = spawn(codexBin, codexArgs, {
|
|
43
|
+
cwd,
|
|
44
|
+
env,
|
|
45
|
+
stdio: "inherit"
|
|
46
|
+
});
|
|
47
|
+
codex.on("error", (err) => {
|
|
48
|
+
console.error("\u274C \u542F\u52A8 Codex \u5931\u8D25:", err.message);
|
|
49
|
+
console.error("");
|
|
50
|
+
console.error("\u8BF7\u786E\u4FDD\u5DF2\u5B89\u88C5 Codex CLI:");
|
|
51
|
+
console.error(" npm install -g @openai/codex");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
});
|
|
54
|
+
codex.on("exit", (code) => {
|
|
55
|
+
if (code !== 0 && code !== null) {
|
|
56
|
+
console.error(`
|
|
57
|
+
\u274C Codex \u9000\u51FA\uFF0C\u4EE3\u7801: ${code}`);
|
|
58
|
+
process.exit(code);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function resolveCodexPath() {
|
|
63
|
+
return process.env.CODEX_PATH || getConfigValue("CODEX_PATH") || "codex";
|
|
64
|
+
}
|
|
65
|
+
function isManagedSession() {
|
|
66
|
+
return process.env.workspacecord_MANAGED === "1";
|
|
67
|
+
}
|
|
68
|
+
function getManagedSessionCwd() {
|
|
69
|
+
return process.env.workspacecord_SESSION_CWD;
|
|
70
|
+
}
|
|
71
|
+
async function handleCodexCommand(args) {
|
|
72
|
+
const options = {};
|
|
73
|
+
for (let i = 0; i < args.length; i++) {
|
|
74
|
+
const arg = args[i];
|
|
75
|
+
switch (arg) {
|
|
76
|
+
case "--cwd":
|
|
77
|
+
options.cwd = args[++i];
|
|
78
|
+
break;
|
|
79
|
+
case "--model":
|
|
80
|
+
options.model = args[++i];
|
|
81
|
+
break;
|
|
82
|
+
case "--sandbox-mode":
|
|
83
|
+
options.sandboxMode = args[++i];
|
|
84
|
+
break;
|
|
85
|
+
case "--approval-policy":
|
|
86
|
+
options.approvalPolicy = args[++i];
|
|
87
|
+
break;
|
|
88
|
+
case "--help":
|
|
89
|
+
case "-h":
|
|
90
|
+
printHelp();
|
|
91
|
+
return;
|
|
92
|
+
default:
|
|
93
|
+
if (!options.args) options.args = [];
|
|
94
|
+
options.args.push(arg);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
await launchManagedCodex(options);
|
|
98
|
+
}
|
|
99
|
+
function printHelp() {
|
|
100
|
+
console.log(`
|
|
101
|
+
\x1B[1mworkspacecord codex\x1B[0m \u2014 \u542F\u52A8\u53D7\u7BA1 Codex \u4F1A\u8BDD
|
|
102
|
+
|
|
103
|
+
\x1B[1m\u7528\u6CD5:\x1B[0m
|
|
104
|
+
workspacecord codex [\u9009\u9879]
|
|
105
|
+
|
|
106
|
+
\x1B[1m\u9009\u9879:\x1B[0m
|
|
107
|
+
--cwd <path> \u5DE5\u4F5C\u76EE\u5F55\uFF08\u9ED8\u8BA4\uFF1A\u5F53\u524D\u76EE\u5F55\uFF09
|
|
108
|
+
--model <model> \u6A21\u578B\u540D\u79F0\uFF08\u5982\uFF1Agpt-4\uFF09
|
|
109
|
+
--sandbox-mode <mode> \u6C99\u7BB1\u6A21\u5F0F\uFF1Aread-only | workspace-write | danger-full-access
|
|
110
|
+
--approval-policy <policy> \u5BA1\u6279\u7B56\u7565\uFF1Anever | on-request | on-failure | untrusted
|
|
111
|
+
--help, -h \u663E\u793A\u6B64\u5E2E\u52A9\u4FE1\u606F
|
|
112
|
+
|
|
113
|
+
\x1B[1m\u53D7\u7BA1\u4F1A\u8BDD\u7279\u6027:\x1B[0m
|
|
114
|
+
\u2713 \u5FEB\u901F\u53D1\u73B0 - \u4F1A\u8BDD\u81EA\u52A8\u540C\u6B65\u5230 Discord
|
|
115
|
+
\u2713 \u72B6\u6001\u76D1\u63A7 - \u5B9E\u65F6\u663E\u793A\u6267\u884C\u72B6\u6001
|
|
116
|
+
\u2713 \u8FDC\u7A0B\u5BA1\u6279 - \u5728 Discord \u4E2D\u5141\u8BB8/\u62D2\u7EDD\u64CD\u4F5C
|
|
117
|
+
\u2713 \u7EC8\u7AEF\u5904\u7406 - \u4ECD\u53EF\u5728\u7EC8\u7AEF\u76F4\u63A5\u5904\u7406
|
|
118
|
+
|
|
119
|
+
\x1B[1m\u793A\u4F8B:\x1B[0m
|
|
120
|
+
workspacecord codex
|
|
121
|
+
workspacecord codex --cwd /path/to/project
|
|
122
|
+
workspacecord codex --model gpt-4 --sandbox-mode workspace-write
|
|
123
|
+
|
|
124
|
+
\x1B[2m\u66F4\u591A\u4FE1\u606F: https://github.com/xuhongbo/workspacecord\x1B[0m
|
|
125
|
+
`);
|
|
126
|
+
}
|
|
127
|
+
export {
|
|
128
|
+
getManagedSessionCwd,
|
|
129
|
+
handleCodexCommand,
|
|
130
|
+
isManagedSession,
|
|
131
|
+
launchManagedCodex
|
|
132
|
+
};
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
config
|
|
4
|
+
} from "./chunk-2LBNM64L.js";
|
|
5
|
+
import "./chunk-OKI4UVGY.js";
|
|
6
|
+
import "./chunk-K3NQKI34.js";
|
|
7
|
+
|
|
8
|
+
// src/providers/codex-provider.ts
|
|
9
|
+
import { writeFileSync, readFileSync, unlinkSync, existsSync, mkdtempSync } from "fs";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
import { tmpdir } from "os";
|
|
12
|
+
|
|
13
|
+
// src/providers/codex/helpers.ts
|
|
14
|
+
function buildCodexOptions() {
|
|
15
|
+
const codexOpts = {};
|
|
16
|
+
if (config.codexApiKey) codexOpts.apiKey = config.codexApiKey;
|
|
17
|
+
if (config.codexBaseUrl) codexOpts.baseUrl = config.codexBaseUrl;
|
|
18
|
+
if (config.codexPath) codexOpts.codexPathOverride = config.codexPath;
|
|
19
|
+
return codexOpts;
|
|
20
|
+
}
|
|
21
|
+
function buildThreadOptions(options) {
|
|
22
|
+
const threadOptions = {
|
|
23
|
+
workingDirectory: options.directory,
|
|
24
|
+
skipGitRepoCheck: true
|
|
25
|
+
};
|
|
26
|
+
if (options.model) threadOptions.model = options.model;
|
|
27
|
+
if (options.sandboxMode) threadOptions.sandboxMode = options.sandboxMode;
|
|
28
|
+
if (options.approvalPolicy) threadOptions.approvalPolicy = options.approvalPolicy;
|
|
29
|
+
if (options.networkAccessEnabled !== void 0) {
|
|
30
|
+
threadOptions.networkAccessEnabled = options.networkAccessEnabled;
|
|
31
|
+
}
|
|
32
|
+
if (options.webSearchMode && options.webSearchMode !== "disabled") {
|
|
33
|
+
threadOptions.webSearchMode = options.webSearchMode;
|
|
34
|
+
}
|
|
35
|
+
if (options.modelReasoningEffort) {
|
|
36
|
+
threadOptions.modelReasoningEffort = options.modelReasoningEffort;
|
|
37
|
+
}
|
|
38
|
+
return threadOptions;
|
|
39
|
+
}
|
|
40
|
+
function parseFileChanges(item) {
|
|
41
|
+
const raw = item.changes || item.files || [];
|
|
42
|
+
return raw.map((f) => ({
|
|
43
|
+
filePath: String(f.path || f.file_path || f.filePath || ""),
|
|
44
|
+
changeKind: f.kind || f.change_kind || f.changeKind || "update"
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
function parseTodoItems(item) {
|
|
48
|
+
const raw = item.items || item.todos || [];
|
|
49
|
+
return raw.map((t) => ({
|
|
50
|
+
text: String(t.text || t.description || ""),
|
|
51
|
+
completed: Boolean(t.completed ?? t.done ?? false)
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/providers/codex-provider.ts
|
|
56
|
+
var Codex = null;
|
|
57
|
+
async function loadSdk() {
|
|
58
|
+
if (Codex) return;
|
|
59
|
+
const mod = await import("@openai/codex-sdk");
|
|
60
|
+
Codex = mod.Codex;
|
|
61
|
+
}
|
|
62
|
+
var SENTINEL_START = "<!-- workspacecord-persona-start -->";
|
|
63
|
+
var SENTINEL_END = "<!-- workspacecord-persona-end -->";
|
|
64
|
+
function stripInjectedAgentsMd(content) {
|
|
65
|
+
return content.replace(
|
|
66
|
+
new RegExp(`${escapeRegex(SENTINEL_START)}[\\s\\S]*?${escapeRegex(SENTINEL_END)}\\n?`, "g"),
|
|
67
|
+
""
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
function injectAgentsMd(directory, parts) {
|
|
71
|
+
if (parts.length === 0) return null;
|
|
72
|
+
const agentsPath = join(directory, "AGENTS.md");
|
|
73
|
+
const injected = `${SENTINEL_START}
|
|
74
|
+
${parts.join("\n\n")}
|
|
75
|
+
${SENTINEL_END}`;
|
|
76
|
+
const existed = existsSync(agentsPath);
|
|
77
|
+
const current = existed ? readFileSync(agentsPath, "utf-8") : "";
|
|
78
|
+
const cleaned = stripInjectedAgentsMd(current).trimEnd();
|
|
79
|
+
writeFileSync(agentsPath, cleaned ? `${cleaned}
|
|
80
|
+
|
|
81
|
+
${injected}
|
|
82
|
+
` : `${injected}
|
|
83
|
+
`, "utf-8");
|
|
84
|
+
return { existed };
|
|
85
|
+
}
|
|
86
|
+
function restoreAgentsMd(directory, state) {
|
|
87
|
+
if (!state) return;
|
|
88
|
+
const agentsPath = join(directory, "AGENTS.md");
|
|
89
|
+
if (!existsSync(agentsPath)) return;
|
|
90
|
+
const cleaned = stripInjectedAgentsMd(readFileSync(agentsPath, "utf-8")).trimEnd();
|
|
91
|
+
if (!state.existed && !cleaned) {
|
|
92
|
+
try {
|
|
93
|
+
unlinkSync(agentsPath);
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
writeFileSync(agentsPath, cleaned ? `${cleaned}
|
|
99
|
+
` : "", "utf-8");
|
|
100
|
+
}
|
|
101
|
+
function escapeRegex(s) {
|
|
102
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
103
|
+
}
|
|
104
|
+
function writeImagesToTemp(blocks) {
|
|
105
|
+
const textParts = [];
|
|
106
|
+
const localImages = [];
|
|
107
|
+
for (const block of blocks) {
|
|
108
|
+
if (block.type === "text") {
|
|
109
|
+
textParts.push(block.text);
|
|
110
|
+
} else if (block.type === "image") {
|
|
111
|
+
const dir = mkdtempSync(join(tmpdir(), "workspacecord-img-"));
|
|
112
|
+
const ext = block.source.media_type.split("/")[1] || "png";
|
|
113
|
+
const filePath = join(dir, `image.${ext}`);
|
|
114
|
+
writeFileSync(filePath, Buffer.from(block.source.data, "base64"));
|
|
115
|
+
localImages.push({ type: "local_image", path: filePath });
|
|
116
|
+
} else if (block.type === "local_image") {
|
|
117
|
+
localImages.push(block);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { textParts, localImages };
|
|
121
|
+
}
|
|
122
|
+
var CodexProvider = class {
|
|
123
|
+
name = "codex";
|
|
124
|
+
supports(feature) {
|
|
125
|
+
return ["command_execution", "file_changes", "reasoning", "todo_list", "continue"].includes(
|
|
126
|
+
feature
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
async *sendPrompt(prompt, options) {
|
|
130
|
+
await loadSdk();
|
|
131
|
+
let input;
|
|
132
|
+
if (typeof prompt === "string") {
|
|
133
|
+
input = prompt;
|
|
134
|
+
} else {
|
|
135
|
+
const { textParts, localImages } = writeImagesToTemp(prompt);
|
|
136
|
+
const inputParts = [];
|
|
137
|
+
for (const img of localImages) {
|
|
138
|
+
inputParts.push({ type: "local_image", path: img.path });
|
|
139
|
+
}
|
|
140
|
+
if (textParts.length > 0) {
|
|
141
|
+
inputParts.push({ type: "text", text: textParts.join("\n") });
|
|
142
|
+
}
|
|
143
|
+
input = inputParts.length === 1 && inputParts[0].type === "text" ? inputParts[0].text ?? "" : inputParts;
|
|
144
|
+
}
|
|
145
|
+
let originalAgents = null;
|
|
146
|
+
try {
|
|
147
|
+
originalAgents = injectAgentsMd(options.directory, options.systemPromptParts);
|
|
148
|
+
const codex = new Codex(buildCodexOptions());
|
|
149
|
+
const threadOptions = buildThreadOptions(options);
|
|
150
|
+
const thread = options.providerSessionId ? codex.resumeThread(options.providerSessionId, threadOptions) : codex.startThread(threadOptions);
|
|
151
|
+
const { events } = await thread.runStreamed(input);
|
|
152
|
+
yield* this.translateEvents(events, options.abortController);
|
|
153
|
+
} finally {
|
|
154
|
+
restoreAgentsMd(options.directory, originalAgents);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async *continueSession(options) {
|
|
158
|
+
await loadSdk();
|
|
159
|
+
if (!options.providerSessionId) {
|
|
160
|
+
yield { type: "error", message: "No session to continue \u2014 no previous thread ID." };
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
let originalAgents = null;
|
|
164
|
+
try {
|
|
165
|
+
originalAgents = injectAgentsMd(options.directory, options.systemPromptParts);
|
|
166
|
+
const codex = new Codex(buildCodexOptions());
|
|
167
|
+
const thread = codex.resumeThread(options.providerSessionId, buildThreadOptions(options));
|
|
168
|
+
const { events } = await thread.runStreamed("Continue from where you left off.");
|
|
169
|
+
yield* this.translateEvents(events, options.abortController);
|
|
170
|
+
} finally {
|
|
171
|
+
restoreAgentsMd(options.directory, originalAgents);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async *translateEvents(events, abortController) {
|
|
175
|
+
const messageText = /* @__PURE__ */ new Map();
|
|
176
|
+
const startTime = Date.now();
|
|
177
|
+
try {
|
|
178
|
+
for await (const event of events) {
|
|
179
|
+
if (abortController.signal.aborted) break;
|
|
180
|
+
switch (event.type) {
|
|
181
|
+
case "thread.started":
|
|
182
|
+
yield { type: "session_init", providerSessionId: event.thread_id || "" };
|
|
183
|
+
break;
|
|
184
|
+
case "item.started":
|
|
185
|
+
case "item.updated": {
|
|
186
|
+
const item = event.item;
|
|
187
|
+
if (!item) break;
|
|
188
|
+
if (item.type === "agent_message") {
|
|
189
|
+
const itemId = String(item.id || "");
|
|
190
|
+
const prev = messageText.get(itemId) || "";
|
|
191
|
+
const text = String(item.text || "");
|
|
192
|
+
if (text.length > prev.length) {
|
|
193
|
+
yield { type: "text_delta", text: text.slice(prev.length) };
|
|
194
|
+
messageText.set(itemId, text);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (item.type === "reasoning" && event.type === "item.updated") {
|
|
198
|
+
const text = String(item.summary || item.content || "");
|
|
199
|
+
if (text) yield { type: "reasoning", text };
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case "item.completed": {
|
|
204
|
+
const item = event.item;
|
|
205
|
+
if (!item) break;
|
|
206
|
+
switch (item.type) {
|
|
207
|
+
case "agent_message": {
|
|
208
|
+
const itemId = String(item.id || "");
|
|
209
|
+
const prev = messageText.get(itemId) || "";
|
|
210
|
+
const text = String(item.text || "");
|
|
211
|
+
if (text.length > prev.length) {
|
|
212
|
+
yield { type: "text_delta", text: text.slice(prev.length) };
|
|
213
|
+
}
|
|
214
|
+
messageText.delete(itemId);
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
case "command_execution":
|
|
218
|
+
yield {
|
|
219
|
+
type: "command_execution",
|
|
220
|
+
command: String(item.command || ""),
|
|
221
|
+
output: String(item.aggregated_output ?? item.output ?? ""),
|
|
222
|
+
exitCode: typeof item.exit_code === "number" ? item.exit_code : typeof item.exitCode === "number" ? item.exitCode : null,
|
|
223
|
+
status: String(item.status || "completed")
|
|
224
|
+
};
|
|
225
|
+
break;
|
|
226
|
+
case "file_change": {
|
|
227
|
+
const changes = parseFileChanges(item);
|
|
228
|
+
if (changes.length > 0) yield { type: "file_change", changes };
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
case "reasoning": {
|
|
232
|
+
const text = String(item.summary || item.content || "");
|
|
233
|
+
if (text) yield { type: "reasoning", text };
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
case "todo_list": {
|
|
237
|
+
const items = parseTodoItems(item);
|
|
238
|
+
if (items.length > 0) yield { type: "todo_list", items };
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case "mcp_tool_call":
|
|
242
|
+
yield {
|
|
243
|
+
type: "tool_start",
|
|
244
|
+
toolName: `${String(item.server || "")}/${String(item.tool || "")}`,
|
|
245
|
+
toolInput: JSON.stringify(item.arguments || item.input || {})
|
|
246
|
+
};
|
|
247
|
+
if (item.status === "completed" || item.status === "failed") {
|
|
248
|
+
yield {
|
|
249
|
+
type: "tool_result",
|
|
250
|
+
toolName: `${String(item.server || "")}/${String(item.tool || "")}`,
|
|
251
|
+
result: typeof item.output === "string" ? item.output : JSON.stringify(item.output || ""),
|
|
252
|
+
isError: item.status === "failed"
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
break;
|
|
256
|
+
case "web_search":
|
|
257
|
+
yield { type: "web_search", query: String(item.query || "") };
|
|
258
|
+
break;
|
|
259
|
+
case "error":
|
|
260
|
+
yield { type: "error", message: String(item.message || "Unknown error") };
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
case "turn.completed": {
|
|
266
|
+
const usage = event.usage;
|
|
267
|
+
const inputTokens = usage?.input_tokens || 0;
|
|
268
|
+
const outputTokens = usage?.output_tokens || 0;
|
|
269
|
+
const costUsd = (inputTokens * 2 + outputTokens * 8) / 1e6;
|
|
270
|
+
yield {
|
|
271
|
+
type: "result",
|
|
272
|
+
success: true,
|
|
273
|
+
costUsd,
|
|
274
|
+
durationMs: Date.now() - startTime,
|
|
275
|
+
numTurns: 1,
|
|
276
|
+
errors: []
|
|
277
|
+
};
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
case "turn.failed":
|
|
281
|
+
yield {
|
|
282
|
+
type: "result",
|
|
283
|
+
success: false,
|
|
284
|
+
costUsd: 0,
|
|
285
|
+
durationMs: Date.now() - startTime,
|
|
286
|
+
numTurns: 1,
|
|
287
|
+
errors: [event.error || "Turn failed"]
|
|
288
|
+
};
|
|
289
|
+
break;
|
|
290
|
+
case "error":
|
|
291
|
+
yield { type: "error", message: event.message || "Unknown error" };
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} catch (err) {
|
|
296
|
+
if (!abortController.signal.aborted) {
|
|
297
|
+
yield { type: "error", message: err.message || "Codex stream error" };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
export {
|
|
303
|
+
CodexProvider
|
|
304
|
+
};
|