codex-task 0.2.2 → 0.2.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.
Files changed (49) hide show
  1. package/.codex-plugin/plugin.json +4 -4
  2. package/README.md +201 -160
  3. package/README_EN.md +196 -0
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/api.js +36 -18
  6. package/dist/api.js.map +1 -1
  7. package/dist/backends/direct/index.d.ts +6 -2
  8. package/dist/backends/direct/index.d.ts.map +1 -1
  9. package/dist/backends/direct/index.js +2 -1
  10. package/dist/backends/direct/index.js.map +1 -1
  11. package/dist/backends/direct/protocol.d.ts +1 -0
  12. package/dist/backends/direct/protocol.d.ts.map +1 -1
  13. package/dist/backends/direct/protocol.js +1 -1
  14. package/dist/backends/direct/protocol.js.map +1 -1
  15. package/dist/cli.js +98 -35
  16. package/dist/cli.js.map +1 -1
  17. package/dist/images.d.ts +4 -2
  18. package/dist/images.d.ts.map +1 -1
  19. package/dist/images.js +35 -26
  20. package/dist/images.js.map +1 -1
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/inputs.d.ts +14 -0
  26. package/dist/inputs.d.ts.map +1 -0
  27. package/dist/inputs.js +40 -0
  28. package/dist/inputs.js.map +1 -0
  29. package/dist/server.d.ts +19 -0
  30. package/dist/server.d.ts.map +1 -0
  31. package/dist/server.js +503 -0
  32. package/dist/server.js.map +1 -0
  33. package/dist/types.d.ts +8 -6
  34. package/dist/types.d.ts.map +1 -1
  35. package/examples/mobile/README.md +69 -0
  36. package/examples/mobile/android/CodexTaskClient.kt +126 -0
  37. package/examples/mobile/android/MealWorkflow.kt +51 -0
  38. package/examples/mobile/ios/CodexTaskClient.swift +163 -0
  39. package/examples/mobile/ios/MealWorkflow.swift +55 -0
  40. package/package.json +4 -2
  41. package/scripts/service/Install-Windows.ps1 +47 -0
  42. package/scripts/service/Uninstall-Windows.ps1 +9 -0
  43. package/scripts/service/install-macos.sh +66 -0
  44. package/scripts/service/install-ubuntu.sh +63 -0
  45. package/scripts/service/uninstall-macos.sh +12 -0
  46. package/scripts/service/uninstall-ubuntu.sh +12 -0
  47. package/skills/codex-task/SKILL.md +74 -38
  48. package/skills/codex-task/agents/openai.yaml +1 -1
  49. package/README.zh-CN.md +0 -209
@@ -0,0 +1,69 @@
1
+ # CodexTask 手机端示例
2
+
3
+ 这里的 Android Kotlin 和 iOS Swift 客户端调用已经运行的 `codex-task serve`。推荐连接可信局域网、Tailscale/WireGuard,或带 HTTPS 的反向代理。
4
+
5
+ 不要把生产 Service Token 提交到 Git,也不要硬编码到公开 App。示例构造器接收 token,私有 App 可以从 Android Keystore、iOS Keychain、受管配置或用户输入中读取。
6
+
7
+ ## 完整示例流程
8
+
9
+ 两个 `MealWorkflow` 都会实际串起四类任务:
10
+
11
+ 1. `POST /v1/image`:根据文字和 `style.md` 生成健身营养餐。
12
+ 2. 轮询 job,读取 `result.artifacts[0].downloadUrl`,携带同一 token 下载图片。
13
+ 3. `POST /v1/text`:把图片和 JSON Schema 一起上传,做图生文营养分析。
14
+ 4. `POST /v1/task`:把营养 JSON、参考图片和服务器上的项目目录交给 Codex SDK 修改。
15
+ 5. 如果返回 `needs_input`,使用 `result.taskId` 调用 `POST /v1/tasks/:taskId/resume`。
16
+
17
+ 所有提交先返回 `202`、`jobId` 和 `statusUrl`。客户端轮询 `GET statusUrl`,直到 `completed`、`needs_input`、`failed` 或 `cancelled`。这种设计不会让手机保持一个可能持续数十分钟的 HTTP 请求。
18
+
19
+ 命名 prompt 文档格式为 `{name, content}`;图片格式为 `{name, mimeType, dataBase64}`。text、image、task 和 resume 都能组合文本、多份文档与多张图片。
20
+
21
+ ## Android
22
+
23
+ 文件:
24
+
25
+ - [`android/CodexTaskClient.kt`](./android/CodexTaskClient.kt):OkHttp + coroutines 客户端,包含四类提交、轮询与 artifact 下载。
26
+ - [`android/MealWorkflow.kt`](./android/MealWorkflow.kt):完整营养餐流程。
27
+
28
+ Gradle 依赖:
29
+
30
+ ```kotlin
31
+ implementation("com.squareup.okhttp3:okhttp:4.12.0")
32
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
33
+ ```
34
+
35
+ 从 ViewModel 或协程中调用:
36
+
37
+ ```kotlin
38
+ val client = CodexTaskClient("http://192.168.1.50:7777", tokenFromKeystore)
39
+ val result = runMealWorkflow(client, "/absolute/server/path/to/meal-app")
40
+ ```
41
+
42
+ Android 默认阻止明文 HTTP。生产环境应使用 HTTPS 或 VPN;仅在可信局域网开发时,才通过范围收窄的 Network Security Config 放行指定主机,不要全局允许明文流量。
43
+
44
+ ## iOS
45
+
46
+ 文件:
47
+
48
+ - [`ios/CodexTaskClient.swift`](./ios/CodexTaskClient.swift):Foundation `URLSession` async/await 客户端。
49
+ - [`ios/MealWorkflow.swift`](./ios/MealWorkflow.swift):与 Android 相同的完整流程。
50
+
51
+ 调用示例:
52
+
53
+ ```swift
54
+ let client = CodexTaskClient(baseURL: URL(string: "http://192.168.1.50:7777")!, token: tokenFromKeychain)
55
+ let result = try await runMealWorkflow(client: client, serverProjectPath: "/absolute/server/path/to/meal-app")
56
+ ```
57
+
58
+ iOS App Transport Security 默认阻止明文 HTTP。生产环境应使用 HTTPS 或 VPN;局域网调试时只为目标域名配置 ATS 开发例外。
59
+
60
+ ## 服务地址
61
+
62
+ `0.0.0.0` 是电脑的监听地址,不是手机可以访问的目标地址。App 应配置电脑可达的地址,例如:
63
+
64
+ ```text
65
+ http://192.168.1.50:7777
66
+ https://codex-task.example.internal
67
+ ```
68
+
69
+ 平台安装脚本打印的 token 通过 `Authorization: Bearer <token>` 发送。持有 token 的手机可以触发服务器上该用户权限范围内的 CodexTask;尤其要谨慎开放 `/v1/task`。
@@ -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.2",
3
+ "version": "0.2.4",
4
4
  "description": "Unofficial agent-to-agent text, image, and workspace task runner for Codex",
5
5
  "type": "module",
6
6
  "author": "wang121ye",
@@ -44,9 +44,11 @@
44
44
  "files": [
45
45
  "dist",
46
46
  "skills",
47
+ "scripts/service",
48
+ "examples/mobile",
47
49
  ".codex-plugin",
48
50
  "README.md",
49
- "README.zh-CN.md",
51
+ "README_EN.md",
50
52
  "LICENSE",
51
53
  "THIRD_PARTY_NOTICES.md"
52
54
  ],
@@ -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/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g; s/'"'"'/\&apos;/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"