codex-task 0.2.3 → 0.2.5
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/.codex-plugin/plugin.json +1 -1
- package/README.md +94 -26
- package/README_EN.md +59 -21
- package/dist/cli.js +50 -0
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +19 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +503 -0
- package/dist/server.js.map +1 -0
- package/examples/mobile/README.md +69 -0
- package/examples/mobile/android/CodexTaskClient.kt +126 -0
- package/examples/mobile/android/MealWorkflow.kt +51 -0
- package/examples/mobile/ios/CodexTaskClient.swift +163 -0
- package/examples/mobile/ios/MealWorkflow.swift +55 -0
- package/package.json +3 -1
- package/scripts/service/Install-Windows.ps1 +47 -0
- package/scripts/service/Uninstall-Windows.ps1 +9 -0
- package/scripts/service/install-macos.sh +66 -0
- package/scripts/service/install-ubuntu.sh +63 -0
- package/scripts/service/install.sh +22 -0
- package/scripts/service/uninstall-macos.sh +12 -0
- package/scripts/service/uninstall-ubuntu.sh +12 -0
- package/scripts/service/uninstall.sh +22 -0
- package/skills/codex-task/SKILL.md +15 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
package xin.wangye.codextask
|
|
2
|
+
|
|
3
|
+
import android.util.Base64
|
|
4
|
+
import kotlinx.coroutines.Dispatchers
|
|
5
|
+
import kotlinx.coroutines.delay
|
|
6
|
+
import kotlinx.coroutines.withContext
|
|
7
|
+
import okhttp3.MediaType.Companion.toMediaType
|
|
8
|
+
import okhttp3.OkHttpClient
|
|
9
|
+
import okhttp3.Request
|
|
10
|
+
import okhttp3.RequestBody.Companion.toRequestBody
|
|
11
|
+
import org.json.JSONArray
|
|
12
|
+
import org.json.JSONObject
|
|
13
|
+
|
|
14
|
+
data class PromptDocument(val name: String, val content: String)
|
|
15
|
+
data class RemoteImage(val name: String, val mimeType: String, val bytes: ByteArray)
|
|
16
|
+
data class JobReceipt(val jobId: String, val statusUrl: String)
|
|
17
|
+
|
|
18
|
+
class CodexTaskClient(
|
|
19
|
+
baseUrl: String,
|
|
20
|
+
private val token: String,
|
|
21
|
+
private val http: OkHttpClient = OkHttpClient(),
|
|
22
|
+
) {
|
|
23
|
+
private val baseUrl = baseUrl.trimEnd('/')
|
|
24
|
+
private val jsonMediaType = "application/json; charset=utf-8".toMediaType()
|
|
25
|
+
private val terminal = setOf("completed", "needs_input", "failed", "cancelled")
|
|
26
|
+
|
|
27
|
+
suspend fun submitText(
|
|
28
|
+
prompt: String,
|
|
29
|
+
promptFiles: List<PromptDocument> = emptyList(),
|
|
30
|
+
images: List<RemoteImage> = emptyList(),
|
|
31
|
+
schema: JSONObject? = null,
|
|
32
|
+
): JobReceipt = submit("/v1/text", payload(prompt, promptFiles, images).apply {
|
|
33
|
+
put("backend", "direct")
|
|
34
|
+
put("reasoning", "medium")
|
|
35
|
+
if (schema != null) put("schema", schema)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
suspend fun submitImage(
|
|
39
|
+
prompt: String,
|
|
40
|
+
promptFiles: List<PromptDocument> = emptyList(),
|
|
41
|
+
images: List<RemoteImage> = emptyList(),
|
|
42
|
+
quality: String = "high",
|
|
43
|
+
): JobReceipt = submit("/v1/image", payload(prompt, promptFiles, images).apply {
|
|
44
|
+
put("backend", "direct")
|
|
45
|
+
put("quality", quality)
|
|
46
|
+
put("count", 1)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
suspend fun submitTask(
|
|
50
|
+
prompt: String,
|
|
51
|
+
workingDirectory: String,
|
|
52
|
+
promptFiles: List<PromptDocument> = emptyList(),
|
|
53
|
+
images: List<RemoteImage> = emptyList(),
|
|
54
|
+
): JobReceipt = submit("/v1/task", payload(prompt, promptFiles, images).apply {
|
|
55
|
+
put("workingDirectory", workingDirectory)
|
|
56
|
+
put("sandboxMode", "danger-full-access")
|
|
57
|
+
put("networkAccess", true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
suspend fun resume(
|
|
61
|
+
taskId: String,
|
|
62
|
+
answer: String,
|
|
63
|
+
promptFiles: List<PromptDocument> = emptyList(),
|
|
64
|
+
images: List<RemoteImage> = emptyList(),
|
|
65
|
+
): JobReceipt = submit("/v1/tasks/$taskId/resume", payload(answer, promptFiles, images))
|
|
66
|
+
|
|
67
|
+
suspend fun awaitJob(receipt: JobReceipt, pollEveryMillis: Long = 1_000): JSONObject {
|
|
68
|
+
while (true) {
|
|
69
|
+
val snapshot = getJson(receipt.statusUrl)
|
|
70
|
+
if (snapshot.getString("status") in terminal) return snapshot
|
|
71
|
+
delay(pollEveryMillis)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
suspend fun downloadArtifact(downloadUrl: String): ByteArray = withContext(Dispatchers.IO) {
|
|
76
|
+
val request = authorized(Request.Builder().url(resolve(downloadUrl))).get().build()
|
|
77
|
+
http.newCall(request).execute().use { response ->
|
|
78
|
+
if (!response.isSuccessful) error("Artifact download failed: HTTP ${response.code}")
|
|
79
|
+
response.body?.bytes() ?: error("Artifact response is empty")
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private suspend fun submit(path: String, body: JSONObject): JobReceipt {
|
|
84
|
+
val json = postJson(path, body)
|
|
85
|
+
return JobReceipt(json.getString("jobId"), json.getString("statusUrl"))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private suspend fun postJson(path: String, body: JSONObject): JSONObject = withContext(Dispatchers.IO) {
|
|
89
|
+
val request = authorized(Request.Builder().url(resolve(path)))
|
|
90
|
+
.post(body.toString().toRequestBody(jsonMediaType))
|
|
91
|
+
.build()
|
|
92
|
+
executeJson(request)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private suspend fun getJson(path: String): JSONObject = withContext(Dispatchers.IO) {
|
|
96
|
+
executeJson(authorized(Request.Builder().url(resolve(path))).get().build())
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private fun executeJson(request: Request): JSONObject {
|
|
100
|
+
http.newCall(request).execute().use { response ->
|
|
101
|
+
val text = response.body?.string().orEmpty()
|
|
102
|
+
if (!response.isSuccessful) error("CodexTask HTTP ${response.code}: $text")
|
|
103
|
+
return JSONObject(text)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private fun payload(
|
|
108
|
+
prompt: String,
|
|
109
|
+
promptFiles: List<PromptDocument>,
|
|
110
|
+
images: List<RemoteImage>,
|
|
111
|
+
): JSONObject = JSONObject().apply {
|
|
112
|
+
put("prompt", prompt)
|
|
113
|
+
put("promptFiles", JSONArray(promptFiles.map { JSONObject().put("name", it.name).put("content", it.content) }))
|
|
114
|
+
put("images", JSONArray(images.map {
|
|
115
|
+
JSONObject()
|
|
116
|
+
.put("name", it.name)
|
|
117
|
+
.put("mimeType", it.mimeType)
|
|
118
|
+
.put("dataBase64", Base64.encodeToString(it.bytes, Base64.NO_WRAP))
|
|
119
|
+
}))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private fun authorized(builder: Request.Builder): Request.Builder =
|
|
123
|
+
builder.header("Authorization", "Bearer $token")
|
|
124
|
+
|
|
125
|
+
private fun resolve(path: String): String = if (path.startsWith("http")) path else "$baseUrl$path"
|
|
126
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
package xin.wangye.codextask
|
|
2
|
+
|
|
3
|
+
import org.json.JSONObject
|
|
4
|
+
|
|
5
|
+
suspend fun runMealWorkflow(
|
|
6
|
+
client: CodexTaskClient,
|
|
7
|
+
serverProjectPath: String,
|
|
8
|
+
): JSONObject {
|
|
9
|
+
val imageJob = client.submitImage(
|
|
10
|
+
prompt = "生成一张俯拍的健身营养餐:煎鸡胸肉、糙米、西兰花、牛油果,写实摄影,干净背景",
|
|
11
|
+
promptFiles = listOf(PromptDocument("style.md", "自然光,食物边界清楚,不要文字和水印")),
|
|
12
|
+
)
|
|
13
|
+
val imageResult = client.awaitJob(imageJob)
|
|
14
|
+
check(imageResult.getString("status") == "completed")
|
|
15
|
+
val imageDownloadUrl = imageResult
|
|
16
|
+
.getJSONObject("result")
|
|
17
|
+
.getJSONArray("artifacts")
|
|
18
|
+
.getJSONObject(0)
|
|
19
|
+
.getString("downloadUrl")
|
|
20
|
+
val mealPng = client.downloadArtifact(imageDownloadUrl)
|
|
21
|
+
val meal = RemoteImage("meal.png", "image/png", mealPng)
|
|
22
|
+
val nutritionSchema = JSONObject(
|
|
23
|
+
"""{"type":"object","properties":{"foods":{"type":"array"},"totalCalories":{"type":"number"}},"required":["foods","totalCalories"],"additionalProperties":false}""",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
val textJob = client.submitText(
|
|
27
|
+
prompt = "识别食物并估算总热量,只返回 JSON",
|
|
28
|
+
images = listOf(meal),
|
|
29
|
+
schema = nutritionSchema,
|
|
30
|
+
)
|
|
31
|
+
val nutrition = client.awaitJob(textJob)
|
|
32
|
+
check(nutrition.getString("status") == "completed")
|
|
33
|
+
|
|
34
|
+
val taskJob = client.submitTask(
|
|
35
|
+
prompt = "根据营养分析实现餐食详情页并运行测试",
|
|
36
|
+
workingDirectory = serverProjectPath,
|
|
37
|
+
promptFiles = listOf(PromptDocument("nutrition.json", nutrition.getJSONObject("result").getString("text"))),
|
|
38
|
+
images = listOf(meal),
|
|
39
|
+
)
|
|
40
|
+
val task = client.awaitJob(taskJob)
|
|
41
|
+
if (task.getString("status") != "needs_input") return task
|
|
42
|
+
|
|
43
|
+
val taskId = task.getJSONObject("result").getString("taskId")
|
|
44
|
+
return client.awaitJob(
|
|
45
|
+
client.resume(
|
|
46
|
+
taskId = taskId,
|
|
47
|
+
answer = "按单人份展示热量",
|
|
48
|
+
images = listOf(meal),
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
struct PromptDocument {
|
|
4
|
+
let name: String
|
|
5
|
+
let content: String
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
struct RemoteImage {
|
|
9
|
+
let name: String
|
|
10
|
+
let mimeType: String
|
|
11
|
+
let data: Data
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
struct JobReceipt {
|
|
15
|
+
let jobId: String
|
|
16
|
+
let statusURL: String
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
enum CodexTaskClientError: Error {
|
|
20
|
+
case invalidResponse
|
|
21
|
+
case http(Int, String)
|
|
22
|
+
case server(String)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
final class CodexTaskClient {
|
|
26
|
+
private let baseURL: URL
|
|
27
|
+
private let token: String
|
|
28
|
+
private let session: URLSession
|
|
29
|
+
private let terminalStatuses = Set(["completed", "needs_input", "failed", "cancelled"])
|
|
30
|
+
|
|
31
|
+
init(baseURL: URL, token: String, session: URLSession = .shared) {
|
|
32
|
+
self.baseURL = baseURL
|
|
33
|
+
self.token = token
|
|
34
|
+
self.session = session
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func submitText(
|
|
38
|
+
prompt: String,
|
|
39
|
+
promptFiles: [PromptDocument] = [],
|
|
40
|
+
images: [RemoteImage] = [],
|
|
41
|
+
schema: [String: Any]? = nil
|
|
42
|
+
) async throws -> JobReceipt {
|
|
43
|
+
var body = payload(prompt: prompt, promptFiles: promptFiles, images: images)
|
|
44
|
+
body["backend"] = "direct"
|
|
45
|
+
body["reasoning"] = "medium"
|
|
46
|
+
if let schema { body["schema"] = schema }
|
|
47
|
+
return try await submit(path: "/v1/text", body: body)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func submitImage(
|
|
51
|
+
prompt: String,
|
|
52
|
+
promptFiles: [PromptDocument] = [],
|
|
53
|
+
images: [RemoteImage] = []
|
|
54
|
+
) async throws -> JobReceipt {
|
|
55
|
+
var body = payload(prompt: prompt, promptFiles: promptFiles, images: images)
|
|
56
|
+
body["backend"] = "direct"
|
|
57
|
+
body["quality"] = "high"
|
|
58
|
+
body["count"] = 1
|
|
59
|
+
return try await submit(path: "/v1/image", body: body)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func submitTask(
|
|
63
|
+
prompt: String,
|
|
64
|
+
workingDirectory: String,
|
|
65
|
+
promptFiles: [PromptDocument] = [],
|
|
66
|
+
images: [RemoteImage] = []
|
|
67
|
+
) async throws -> JobReceipt {
|
|
68
|
+
var body = payload(prompt: prompt, promptFiles: promptFiles, images: images)
|
|
69
|
+
body["workingDirectory"] = workingDirectory
|
|
70
|
+
body["sandboxMode"] = "danger-full-access"
|
|
71
|
+
body["networkAccess"] = true
|
|
72
|
+
return try await submit(path: "/v1/task", body: body)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func resume(
|
|
76
|
+
taskId: String,
|
|
77
|
+
answer: String,
|
|
78
|
+
promptFiles: [PromptDocument] = [],
|
|
79
|
+
images: [RemoteImage] = []
|
|
80
|
+
) async throws -> JobReceipt {
|
|
81
|
+
try await submit(
|
|
82
|
+
path: "/v1/tasks/\(taskId)/resume",
|
|
83
|
+
body: payload(prompt: answer, promptFiles: promptFiles, images: images)
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func awaitJob(_ receipt: JobReceipt, pollEveryNanoseconds: UInt64 = 1_000_000_000) async throws -> [String: Any] {
|
|
88
|
+
while true {
|
|
89
|
+
let snapshot = try await getJSON(path: receipt.statusURL)
|
|
90
|
+
if let status = snapshot["status"] as? String, terminalStatuses.contains(status) { return snapshot }
|
|
91
|
+
try await Task.sleep(nanoseconds: pollEveryNanoseconds)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func downloadArtifact(path: String) async throws -> Data {
|
|
96
|
+
var request = URLRequest(url: try resolve(path))
|
|
97
|
+
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
|
98
|
+
let (data, response) = try await session.data(for: request)
|
|
99
|
+
try validate(response: response, data: data)
|
|
100
|
+
return data
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private func submit(path: String, body: [String: Any]) async throws -> JobReceipt {
|
|
104
|
+
let json = try await postJSON(path: path, body: body)
|
|
105
|
+
guard let jobId = json["jobId"] as? String, let statusURL = json["statusUrl"] as? String else {
|
|
106
|
+
throw CodexTaskClientError.invalidResponse
|
|
107
|
+
}
|
|
108
|
+
return JobReceipt(jobId: jobId, statusURL: statusURL)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private func postJSON(path: String, body: [String: Any]) async throws -> [String: Any] {
|
|
112
|
+
var request = URLRequest(url: try resolve(path))
|
|
113
|
+
request.httpMethod = "POST"
|
|
114
|
+
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
|
115
|
+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
116
|
+
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
|
117
|
+
return try await executeJSON(request)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private func getJSON(path: String) async throws -> [String: Any] {
|
|
121
|
+
var request = URLRequest(url: try resolve(path))
|
|
122
|
+
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
|
123
|
+
return try await executeJSON(request)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private func executeJSON(_ request: URLRequest) async throws -> [String: Any] {
|
|
127
|
+
let (data, response) = try await session.data(for: request)
|
|
128
|
+
try validate(response: response, data: data)
|
|
129
|
+
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
130
|
+
throw CodexTaskClientError.invalidResponse
|
|
131
|
+
}
|
|
132
|
+
return json
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private func validate(response: URLResponse, data: Data) throws {
|
|
136
|
+
guard let http = response as? HTTPURLResponse else { throw CodexTaskClientError.invalidResponse }
|
|
137
|
+
guard (200..<300).contains(http.statusCode) else {
|
|
138
|
+
throw CodexTaskClientError.http(http.statusCode, String(data: data, encoding: .utf8) ?? "")
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private func payload(
|
|
143
|
+
prompt: String,
|
|
144
|
+
promptFiles: [PromptDocument],
|
|
145
|
+
images: [RemoteImage]
|
|
146
|
+
) -> [String: Any] {
|
|
147
|
+
[
|
|
148
|
+
"prompt": prompt,
|
|
149
|
+
"promptFiles": promptFiles.map { ["name": $0.name, "content": $0.content] },
|
|
150
|
+
"images": images.map {
|
|
151
|
+
["name": $0.name, "mimeType": $0.mimeType, "dataBase64": $0.data.base64EncodedString()]
|
|
152
|
+
},
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private func resolve(_ path: String) throws -> URL {
|
|
157
|
+
if let absolute = URL(string: path), absolute.scheme != nil { return absolute }
|
|
158
|
+
guard let resolved = URL(string: path, relativeTo: baseURL)?.absoluteURL else {
|
|
159
|
+
throw CodexTaskClientError.server("Invalid URL: \(path)")
|
|
160
|
+
}
|
|
161
|
+
return resolved
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
func runMealWorkflow(
|
|
4
|
+
client: CodexTaskClient,
|
|
5
|
+
serverProjectPath: String
|
|
6
|
+
) async throws -> [String: Any] {
|
|
7
|
+
let imageReceipt = try await client.submitImage(
|
|
8
|
+
prompt: "生成一张俯拍的健身营养餐:煎鸡胸肉、糙米、西兰花、牛油果,写实摄影,干净背景",
|
|
9
|
+
promptFiles: [PromptDocument(name: "style.md", content: "自然光,食物边界清楚,不要文字和水印")]
|
|
10
|
+
)
|
|
11
|
+
let imageResult = try await client.awaitJob(imageReceipt)
|
|
12
|
+
guard imageResult["status"] as? String == "completed",
|
|
13
|
+
let generated = imageResult["result"] as? [String: Any],
|
|
14
|
+
let artifacts = generated["artifacts"] as? [[String: Any]],
|
|
15
|
+
let downloadPath = artifacts.first?["downloadUrl"] as? String else {
|
|
16
|
+
return imageResult
|
|
17
|
+
}
|
|
18
|
+
let mealPNG = try await client.downloadArtifact(path: downloadPath)
|
|
19
|
+
let meal = RemoteImage(name: "meal.png", mimeType: "image/png", data: mealPNG)
|
|
20
|
+
let schema: [String: Any] = [
|
|
21
|
+
"type": "object",
|
|
22
|
+
"properties": [
|
|
23
|
+
"foods": ["type": "array"],
|
|
24
|
+
"totalCalories": ["type": "number"],
|
|
25
|
+
],
|
|
26
|
+
"required": ["foods", "totalCalories"],
|
|
27
|
+
"additionalProperties": false,
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
let textReceipt = try await client.submitText(
|
|
31
|
+
prompt: "识别食物并估算总热量,只返回 JSON",
|
|
32
|
+
images: [meal],
|
|
33
|
+
schema: schema
|
|
34
|
+
)
|
|
35
|
+
let nutrition = try await client.awaitJob(textReceipt)
|
|
36
|
+
guard nutrition["status"] as? String == "completed" else { return nutrition }
|
|
37
|
+
|
|
38
|
+
let result = nutrition["result"] as? [String: Any]
|
|
39
|
+
let nutritionText = result?["text"] as? String ?? "{}"
|
|
40
|
+
let taskReceipt = try await client.submitTask(
|
|
41
|
+
prompt: "根据营养分析实现餐食详情页并运行测试",
|
|
42
|
+
workingDirectory: serverProjectPath,
|
|
43
|
+
promptFiles: [PromptDocument(name: "nutrition.json", content: nutritionText)],
|
|
44
|
+
images: [meal]
|
|
45
|
+
)
|
|
46
|
+
let task = try await client.awaitJob(taskReceipt)
|
|
47
|
+
guard task["status"] as? String == "needs_input",
|
|
48
|
+
let taskResult = task["result"] as? [String: Any],
|
|
49
|
+
let taskId = taskResult["taskId"] as? String else {
|
|
50
|
+
return task
|
|
51
|
+
}
|
|
52
|
+
return try await client.awaitJob(
|
|
53
|
+
try await client.resume(taskId: taskId, answer: "按单人份展示热量", images: [meal])
|
|
54
|
+
)
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-task",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Unofficial agent-to-agent text, image, and workspace task runner for Codex",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "wang121ye",
|
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
"files": [
|
|
45
45
|
"dist",
|
|
46
46
|
"skills",
|
|
47
|
+
"scripts/service",
|
|
48
|
+
"examples/mobile",
|
|
47
49
|
".codex-plugin",
|
|
48
50
|
"README.md",
|
|
49
51
|
"README_EN.md",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[string]$HostAddress = $(if ($env:CODEX_TASK_SERVICE_HOST) { $env:CODEX_TASK_SERVICE_HOST } else { "0.0.0.0" }),
|
|
3
|
+
[int]$Port = $(if ($env:CODEX_TASK_SERVICE_PORT) { [int]$env:CODEX_TASK_SERVICE_PORT } else { 7777 }),
|
|
4
|
+
[int]$MaxConcurrency = $(if ($env:CODEX_TASK_SERVICE_CONCURRENCY) { [int]$env:CODEX_TASK_SERVICE_CONCURRENCY } else { 2 })
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
$ErrorActionPreference = "Stop"
|
|
8
|
+
$TaskName = "CodexTask"
|
|
9
|
+
$ServiceDir = Join-Path $env:APPDATA "codex-task\service"
|
|
10
|
+
$TokenFile = Join-Path $ServiceDir "token"
|
|
11
|
+
$Runner = Join-Path $ServiceDir "run.cmd"
|
|
12
|
+
$LogFile = Join-Path $ServiceDir "service.log"
|
|
13
|
+
|
|
14
|
+
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) { throw "npm is required" }
|
|
15
|
+
npm install -g codex-task@latest
|
|
16
|
+
if ($LASTEXITCODE -ne 0) { throw "npm install -g codex-task@latest failed" }
|
|
17
|
+
$CodexTask = (Get-Command codex-task.cmd -ErrorAction SilentlyContinue).Source
|
|
18
|
+
if (-not $CodexTask) { $CodexTask = (Get-Command codex-task -ErrorAction Stop).Source }
|
|
19
|
+
|
|
20
|
+
New-Item -ItemType Directory -Path $ServiceDir -Force | Out-Null
|
|
21
|
+
if (-not (Test-Path $TokenFile) -or (Get-Item $TokenFile).Length -eq 0) {
|
|
22
|
+
$Token = node -e "process.stdout.write(require('node:crypto').randomBytes(32).toString('base64url'))"
|
|
23
|
+
[System.IO.File]::WriteAllText($TokenFile, $Token)
|
|
24
|
+
}
|
|
25
|
+
icacls $TokenFile /inheritance:r /grant:r "$env:USERNAME`:(R,W)" | Out-Null
|
|
26
|
+
|
|
27
|
+
$RunnerBody = @"
|
|
28
|
+
@echo off
|
|
29
|
+
"$CodexTask" serve --host "$HostAddress" --port "$Port" --token-file "$TokenFile" --max-concurrency "$MaxConcurrency" >> "$LogFile" 2>&1
|
|
30
|
+
"@
|
|
31
|
+
[System.IO.File]::WriteAllText($Runner, $RunnerBody)
|
|
32
|
+
|
|
33
|
+
$UserId = if ($env:USERDOMAIN) { "$env:USERDOMAIN\$env:USERNAME" } else { $env:USERNAME }
|
|
34
|
+
$Action = New-ScheduledTaskAction -Execute (Join-Path $env:SystemRoot "System32\cmd.exe") -Argument "/d /c `"$Runner`""
|
|
35
|
+
$Trigger = New-ScheduledTaskTrigger -AtLogOn -User $UserId
|
|
36
|
+
$Principal = New-ScheduledTaskPrincipal -UserId $UserId -LogonType Interactive -RunLevel Limited
|
|
37
|
+
$Settings = New-ScheduledTaskSettingsSet -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero)
|
|
38
|
+
$Task = New-ScheduledTask -Action $Action -Trigger $Trigger -Principal $Principal -Settings $Settings -Description "Authenticated CodexTask HTTP service"
|
|
39
|
+
Register-ScheduledTask -TaskName $TaskName -InputObject $Task -Force | Out-Null
|
|
40
|
+
Start-ScheduledTask -TaskName $TaskName
|
|
41
|
+
|
|
42
|
+
Write-Host "CodexTask scheduled task installed."
|
|
43
|
+
Write-Host "URL: http://${HostAddress}:$Port"
|
|
44
|
+
Write-Host "Token: $([System.IO.File]::ReadAllText($TokenFile))"
|
|
45
|
+
Write-Host "Token file: $TokenFile"
|
|
46
|
+
Write-Host "Log: $LogFile"
|
|
47
|
+
Write-Host "The task starts when $UserId logs in so it can reuse that user's Codex environment."
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
$ErrorActionPreference = "Stop"
|
|
2
|
+
$TaskName = "CodexTask"
|
|
3
|
+
$ServiceDir = Join-Path $env:APPDATA "codex-task\service"
|
|
4
|
+
|
|
5
|
+
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
|
6
|
+
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
|
|
7
|
+
Remove-Item -LiteralPath $ServiceDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
8
|
+
|
|
9
|
+
Write-Host "CodexTask scheduled task removed. The global npm package and Codex data were preserved."
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
service_host="${CODEX_TASK_SERVICE_HOST:-0.0.0.0}"
|
|
5
|
+
service_port="${CODEX_TASK_SERVICE_PORT:-7777}"
|
|
6
|
+
service_concurrency="${CODEX_TASK_SERVICE_CONCURRENCY:-2}"
|
|
7
|
+
service_dir="$HOME/Library/Application Support/codex-task/service"
|
|
8
|
+
token_file="$service_dir/token"
|
|
9
|
+
runner="$service_dir/run.sh"
|
|
10
|
+
log_file="$service_dir/service.log"
|
|
11
|
+
label="com.wangyendt.codex-task"
|
|
12
|
+
plist="$HOME/Library/LaunchAgents/$label.plist"
|
|
13
|
+
|
|
14
|
+
command -v npm >/dev/null 2>&1 || { echo "npm is required" >&2; exit 1; }
|
|
15
|
+
npm install -g codex-task@latest
|
|
16
|
+
codex_task_bin="$(command -v codex-task)"
|
|
17
|
+
|
|
18
|
+
mkdir -p "$service_dir" "$HOME/Library/LaunchAgents"
|
|
19
|
+
chmod 700 "$service_dir"
|
|
20
|
+
if [[ ! -s "$token_file" ]]; then
|
|
21
|
+
node -e 'process.stdout.write(require("node:crypto").randomBytes(32).toString("base64url"))' > "$token_file"
|
|
22
|
+
fi
|
|
23
|
+
chmod 600 "$token_file"
|
|
24
|
+
|
|
25
|
+
quoted_path="$(printf '%q' "$PATH")"
|
|
26
|
+
quoted_bin="$(printf '%q' "$codex_task_bin")"
|
|
27
|
+
quoted_host="$(printf '%q' "$service_host")"
|
|
28
|
+
quoted_port="$(printf '%q' "$service_port")"
|
|
29
|
+
quoted_token="$(printf '%q' "$token_file")"
|
|
30
|
+
quoted_concurrency="$(printf '%q' "$service_concurrency")"
|
|
31
|
+
cat > "$runner" <<EOF
|
|
32
|
+
#!/usr/bin/env bash
|
|
33
|
+
export PATH=$quoted_path
|
|
34
|
+
exec $quoted_bin serve --host $quoted_host --port $quoted_port --token-file $quoted_token --max-concurrency $quoted_concurrency
|
|
35
|
+
EOF
|
|
36
|
+
chmod 700 "$runner"
|
|
37
|
+
|
|
38
|
+
xml_escape() {
|
|
39
|
+
printf '%s' "$1" | sed 's/&/\&/g; s/</\</g; s/>/\>/g; s/"/\"/g; s/'"'"'/\'/g'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
cat > "$plist" <<EOF
|
|
43
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
44
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
45
|
+
<plist version="1.0">
|
|
46
|
+
<dict>
|
|
47
|
+
<key>Label</key><string>$label</string>
|
|
48
|
+
<key>ProgramArguments</key>
|
|
49
|
+
<array><string>$(xml_escape "$runner")</string></array>
|
|
50
|
+
<key>RunAtLoad</key><true/>
|
|
51
|
+
<key>KeepAlive</key><true/>
|
|
52
|
+
<key>StandardOutPath</key><string>$(xml_escape "$log_file")</string>
|
|
53
|
+
<key>StandardErrorPath</key><string>$(xml_escape "$log_file")</string>
|
|
54
|
+
</dict>
|
|
55
|
+
</plist>
|
|
56
|
+
EOF
|
|
57
|
+
plutil -lint "$plist" >/dev/null
|
|
58
|
+
|
|
59
|
+
launchctl bootout "gui/$(id -u)/$label" 2>/dev/null || true
|
|
60
|
+
launchctl bootstrap "gui/$(id -u)" "$plist"
|
|
61
|
+
|
|
62
|
+
echo "CodexTask LaunchAgent installed."
|
|
63
|
+
echo "URL: http://$service_host:$service_port"
|
|
64
|
+
echo "Token: $(<"$token_file")"
|
|
65
|
+
echo "Token file: $token_file"
|
|
66
|
+
echo "Log: $log_file"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
service_host="${CODEX_TASK_SERVICE_HOST:-0.0.0.0}"
|
|
5
|
+
service_port="${CODEX_TASK_SERVICE_PORT:-7777}"
|
|
6
|
+
service_concurrency="${CODEX_TASK_SERVICE_CONCURRENCY:-2}"
|
|
7
|
+
service_dir="${XDG_CONFIG_HOME:-$HOME/.config}/codex-task/service"
|
|
8
|
+
user_unit_dir="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
|
9
|
+
token_file="$service_dir/token"
|
|
10
|
+
runner="$service_dir/run.sh"
|
|
11
|
+
unit_file="$user_unit_dir/codex-task.service"
|
|
12
|
+
|
|
13
|
+
command -v npm >/dev/null 2>&1 || { echo "npm is required" >&2; exit 1; }
|
|
14
|
+
command -v systemctl >/dev/null 2>&1 || { echo "systemd user services are required" >&2; exit 1; }
|
|
15
|
+
|
|
16
|
+
npm install -g codex-task@latest
|
|
17
|
+
codex_task_bin="$(command -v codex-task)"
|
|
18
|
+
|
|
19
|
+
mkdir -p "$service_dir" "$user_unit_dir"
|
|
20
|
+
chmod 700 "$service_dir"
|
|
21
|
+
if [[ ! -s "$token_file" ]]; then
|
|
22
|
+
node -e 'process.stdout.write(require("node:crypto").randomBytes(32).toString("base64url"))' > "$token_file"
|
|
23
|
+
fi
|
|
24
|
+
chmod 600 "$token_file"
|
|
25
|
+
|
|
26
|
+
quoted_path="$(printf '%q' "$PATH")"
|
|
27
|
+
quoted_bin="$(printf '%q' "$codex_task_bin")"
|
|
28
|
+
quoted_host="$(printf '%q' "$service_host")"
|
|
29
|
+
quoted_port="$(printf '%q' "$service_port")"
|
|
30
|
+
quoted_token="$(printf '%q' "$token_file")"
|
|
31
|
+
quoted_concurrency="$(printf '%q' "$service_concurrency")"
|
|
32
|
+
cat > "$runner" <<EOF
|
|
33
|
+
#!/usr/bin/env bash
|
|
34
|
+
export PATH=$quoted_path
|
|
35
|
+
exec $quoted_bin serve --host $quoted_host --port $quoted_port --token-file $quoted_token --max-concurrency $quoted_concurrency
|
|
36
|
+
EOF
|
|
37
|
+
chmod 700 "$runner"
|
|
38
|
+
|
|
39
|
+
cat > "$unit_file" <<EOF
|
|
40
|
+
[Unit]
|
|
41
|
+
Description=CodexTask authenticated HTTP service
|
|
42
|
+
After=network-online.target
|
|
43
|
+
Wants=network-online.target
|
|
44
|
+
|
|
45
|
+
[Service]
|
|
46
|
+
Type=simple
|
|
47
|
+
ExecStart="$runner"
|
|
48
|
+
Restart=on-failure
|
|
49
|
+
RestartSec=5
|
|
50
|
+
|
|
51
|
+
[Install]
|
|
52
|
+
WantedBy=default.target
|
|
53
|
+
EOF
|
|
54
|
+
|
|
55
|
+
systemctl --user daemon-reload
|
|
56
|
+
systemctl --user enable --now codex-task.service
|
|
57
|
+
|
|
58
|
+
echo "CodexTask service installed."
|
|
59
|
+
echo "URL: http://$service_host:$service_port"
|
|
60
|
+
echo "Token: $(<"$token_file")"
|
|
61
|
+
echo "Token file: $token_file"
|
|
62
|
+
echo "Status: systemctl --user status codex-task.service"
|
|
63
|
+
echo "For boot-time start before login, an administrator may run: loginctl enable-linger $USER"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
|
5
|
+
echo "Install the CodexTask auto-start service for Ubuntu/Linux or macOS."
|
|
6
|
+
echo "Usage: bash scripts/service/install.sh"
|
|
7
|
+
exit 0
|
|
8
|
+
fi
|
|
9
|
+
if [[ $# -ne 0 ]]; then
|
|
10
|
+
echo "Usage: bash scripts/service/install.sh" >&2
|
|
11
|
+
exit 2
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
+
case "$(uname -s)" in
|
|
16
|
+
Darwin) exec bash "$script_dir/install-macos.sh" ;;
|
|
17
|
+
Linux) exec bash "$script_dir/install-ubuntu.sh" ;;
|
|
18
|
+
*)
|
|
19
|
+
echo "Unsupported platform. This installer supports Ubuntu/Linux and macOS; use Install-Windows.ps1 on Windows." >&2
|
|
20
|
+
exit 1
|
|
21
|
+
;;
|
|
22
|
+
esac
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
label="com.wangyendt.codex-task"
|
|
5
|
+
plist="$HOME/Library/LaunchAgents/$label.plist"
|
|
6
|
+
service_dir="$HOME/Library/Application Support/codex-task/service"
|
|
7
|
+
|
|
8
|
+
launchctl bootout "gui/$(id -u)/$label" 2>/dev/null || true
|
|
9
|
+
rm -f "$plist"
|
|
10
|
+
rm -rf "$service_dir"
|
|
11
|
+
|
|
12
|
+
echo "CodexTask LaunchAgent removed. The global npm package and Codex data were preserved."
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
service_dir="${XDG_CONFIG_HOME:-$HOME/.config}/codex-task/service"
|
|
5
|
+
unit_file="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/codex-task.service"
|
|
6
|
+
|
|
7
|
+
systemctl --user disable --now codex-task.service 2>/dev/null || true
|
|
8
|
+
rm -f "$unit_file"
|
|
9
|
+
systemctl --user daemon-reload
|
|
10
|
+
rm -rf "$service_dir"
|
|
11
|
+
|
|
12
|
+
echo "CodexTask systemd user service removed. The global npm package and Codex data were preserved."
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
|
5
|
+
echo "Uninstall the CodexTask auto-start service for Ubuntu/Linux or macOS."
|
|
6
|
+
echo "Usage: bash scripts/service/uninstall.sh"
|
|
7
|
+
exit 0
|
|
8
|
+
fi
|
|
9
|
+
if [[ $# -ne 0 ]]; then
|
|
10
|
+
echo "Usage: bash scripts/service/uninstall.sh" >&2
|
|
11
|
+
exit 2
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
+
case "$(uname -s)" in
|
|
16
|
+
Darwin) exec bash "$script_dir/uninstall-macos.sh" ;;
|
|
17
|
+
Linux) exec bash "$script_dir/uninstall-ubuntu.sh" ;;
|
|
18
|
+
*)
|
|
19
|
+
echo "Unsupported platform. This uninstaller supports Ubuntu/Linux and macOS; use Uninstall-Windows.ps1 on Windows." >&2
|
|
20
|
+
exit 1
|
|
21
|
+
;;
|
|
22
|
+
esac
|