orynacode-ai 1.16.20 → 1.16.22
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/package.json +1 -1
- package/src/cli/cmd/tui.ts +5 -3
- package/src/oryna/agent-signal.ts +2 -2
- package/src/oryna/agent.ts +0 -124
- package/src/oryna/reply-service.ts +10 -9
- package/src/plugin/oryna.ts +28 -24
- package/src/server/routes/instance/httpapi/groups/session.ts +13 -0
- package/src/server/routes/instance/httpapi/handlers/session.ts +18 -0
- package/src/session/processor.ts +1 -1
package/package.json
CHANGED
package/src/cli/cmd/tui.ts
CHANGED
|
@@ -217,9 +217,11 @@ export const TuiThreadCommand = cmd({
|
|
|
217
217
|
return
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
220
|
+
if (process.env.OPENCODE_CLIENT !== "desktop") {
|
|
221
|
+
setTimeout(() => {
|
|
222
|
+
client.call("checkUpgrade", { directory: cwd }).catch(() => {})
|
|
223
|
+
}, 1000).unref?.()
|
|
224
|
+
}
|
|
223
225
|
|
|
224
226
|
try {
|
|
225
227
|
const { Effect } = await import("effect")
|
|
@@ -7,6 +7,6 @@ export type AgentState = {
|
|
|
7
7
|
url: string
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export const
|
|
11
|
-
createSignal<AgentState>({ connected: false, processing: false, ready: false, url: "" }),
|
|
10
|
+
export const agentStatus = createRoot(() =>
|
|
11
|
+
createSignal<AgentState>({ connected: false, processing: false, ready: false, url: "" })[0],
|
|
12
12
|
)
|
package/src/oryna/agent.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { readFileSync } from "fs"
|
|
2
|
-
import { setAgentStatus, agentStatus } from "./agent-signal"
|
|
3
2
|
import os from "os"
|
|
4
3
|
import path from "path"
|
|
5
4
|
|
|
@@ -21,126 +20,3 @@ export function getOrynaGateKey(dir: string): string {
|
|
|
21
20
|
} catch {}
|
|
22
21
|
return `sk-local-${user}-${path.basename(dir)}`
|
|
23
22
|
}
|
|
24
|
-
|
|
25
|
-
export function setReady(ready: boolean) {
|
|
26
|
-
const s = agentStatus()
|
|
27
|
-
setAgentStatus({ ...s, ready })
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const REPLY_FILE = `/tmp/oryna-reply-${process.pid}`
|
|
31
|
-
let ws: WebSocket | null = null
|
|
32
|
-
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
33
|
-
let heartbeatTimer: ReturnType<typeof setInterval> | null = null
|
|
34
|
-
let replyWatchTimer: ReturnType<typeof setInterval> | null = null
|
|
35
|
-
let stopped = false
|
|
36
|
-
let connecting = false
|
|
37
|
-
let onMessage: ((content: string, from: string) => Promise<void>) | null = null
|
|
38
|
-
|
|
39
|
-
export function setMessageHandler(fn: (content: string, from: string) => Promise<void>) {
|
|
40
|
-
onMessage = fn
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function readAuthUrl(): string | undefined {
|
|
44
|
-
try {
|
|
45
|
-
const dataDir = process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share")
|
|
46
|
-
const authPath = path.join(dataDir, "orynacode", "auth.json")
|
|
47
|
-
const auth = JSON.parse(readFileSync(authPath, "utf-8"))
|
|
48
|
-
const local = auth?.["orynagate"]
|
|
49
|
-
if (local?.metadata?.url) return local.metadata.url
|
|
50
|
-
} catch {}
|
|
51
|
-
return undefined
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function startReplyWatch() {
|
|
55
|
-
if (replyWatchTimer) return
|
|
56
|
-
let offset = 0
|
|
57
|
-
replyWatchTimer = setInterval(() => {
|
|
58
|
-
try {
|
|
59
|
-
const raw = readFileSync(REPLY_FILE, "utf-8")
|
|
60
|
-
const lines = raw.split("\n")
|
|
61
|
-
let sent = 0
|
|
62
|
-
for (let i = offset; i < lines.length; i++) {
|
|
63
|
-
const line = lines[i].trim()
|
|
64
|
-
if (!line) continue
|
|
65
|
-
if (ws?.readyState === WebSocket.OPEN) {
|
|
66
|
-
ws.send(line)
|
|
67
|
-
sent++
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
offset += sent
|
|
71
|
-
} catch {}
|
|
72
|
-
}, 300)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function start() {
|
|
76
|
-
stopped = false
|
|
77
|
-
const url = process.env.ORYNA_GATE_URL || readAuthUrl()
|
|
78
|
-
if (!url) return
|
|
79
|
-
|
|
80
|
-
const host = new URL(url).host
|
|
81
|
-
const token = getOrynaGateKey(process.env.ORYNA_GATE_WORKSPACE || process.cwd())
|
|
82
|
-
const wsUrl = `ws://${host}/ws?token=${token}&name=orynacode`
|
|
83
|
-
|
|
84
|
-
const connect = () => {
|
|
85
|
-
if (connecting) return
|
|
86
|
-
connecting = true
|
|
87
|
-
const socket = new WebSocket(wsUrl)
|
|
88
|
-
ws = socket
|
|
89
|
-
|
|
90
|
-
socket.addEventListener("open", () => {
|
|
91
|
-
startReplyWatch()
|
|
92
|
-
const s = agentStatus()
|
|
93
|
-
setAgentStatus({ connected: true, processing: false, ready: s.ready, url: host })
|
|
94
|
-
heartbeatTimer = setInterval(() => {
|
|
95
|
-
if (socket.readyState === WebSocket.OPEN) socket.send("__PING__")
|
|
96
|
-
}, 30000)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
socket.addEventListener("message", async (event) => {
|
|
100
|
-
try {
|
|
101
|
-
const msg = JSON.parse(event.data.toString())
|
|
102
|
-
if (!msg.success) return
|
|
103
|
-
if (msg.recv === "hello") return
|
|
104
|
-
if (msg.recv === "error") return
|
|
105
|
-
if (msg.recv !== "message") return
|
|
106
|
-
|
|
107
|
-
const content = msg.data?.content
|
|
108
|
-
const from = msg.data?.from || "unknown"
|
|
109
|
-
if (!content) return
|
|
110
|
-
|
|
111
|
-
setTimeout(() => {
|
|
112
|
-
const s = agentStatus()
|
|
113
|
-
setAgentStatus({ connected: true, processing: true, ready: s.ready, url: host })
|
|
114
|
-
}, 0)
|
|
115
|
-
if (onMessage) await onMessage(content, from)
|
|
116
|
-
setTimeout(() => {
|
|
117
|
-
const s = agentStatus()
|
|
118
|
-
setAgentStatus({ connected: true, processing: false, ready: s.ready, url: host })
|
|
119
|
-
}, 0)
|
|
120
|
-
} catch {}
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
socket.addEventListener("close", () => {
|
|
124
|
-
connecting = false
|
|
125
|
-
const s = agentStatus()
|
|
126
|
-
setAgentStatus({ connected: false, processing: false, ready: s.ready, url: host })
|
|
127
|
-
if (!stopped) reconnectTimer = setTimeout(connect, 3000)
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
socket.addEventListener("error", () => {
|
|
131
|
-
socket.close()
|
|
132
|
-
})
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
connect()
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function stop() {
|
|
139
|
-
connecting = false
|
|
140
|
-
stopped = true
|
|
141
|
-
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null }
|
|
142
|
-
if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null }
|
|
143
|
-
if (replyWatchTimer) { clearInterval(replyWatchTimer); replyWatchTimer = null }
|
|
144
|
-
if (ws) { ws.close(); ws = null }
|
|
145
|
-
setAgentStatus({ connected: false, processing: false, ready: false, url: "" })
|
|
146
|
-
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
const REPLY_URL = process.env.ORYNA_GATE_REPLY_URL + "/reply"
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
export async function sendReply(content: string, to?: string) {
|
|
4
|
+
if (!content) return
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
if (REPLY_URL) {
|
|
7
|
+
fetch(REPLY_URL, {
|
|
8
|
+
method: "POST",
|
|
9
|
+
headers: { "Content-Type": "application/json" },
|
|
10
|
+
body: JSON.stringify({ content, to: to ?? "" }),
|
|
11
|
+
}).catch(() => {})
|
|
12
|
+
}
|
|
12
13
|
}
|
package/src/plugin/oryna.ts
CHANGED
|
@@ -291,34 +291,38 @@ export async function OrynaAuthPlugin(input: PluginInput): Promise<Hooks> {
|
|
|
291
291
|
return jwt ? { apiKey: jwt } : {}
|
|
292
292
|
},
|
|
293
293
|
methods: [
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return {
|
|
301
|
-
url: ORYNA_LOGIN_URL,
|
|
302
|
-
instructions: "Complete login in your browser. This window will close automatically when done.",
|
|
303
|
-
method: "auto" as const,
|
|
304
|
-
callback: async () => {
|
|
294
|
+
...(process.env.OPENCODE_CLIENT !== "desktop"
|
|
295
|
+
? [
|
|
296
|
+
{
|
|
297
|
+
type: "oauth",
|
|
298
|
+
label: "Login with Oryna AI (Browser)",
|
|
299
|
+
authorize: async () => {
|
|
305
300
|
try {
|
|
306
|
-
|
|
301
|
+
const tokenPromise = reAuthOryna()
|
|
302
|
+
return {
|
|
303
|
+
url: ORYNA_LOGIN_URL,
|
|
304
|
+
instructions: "Complete login in your browser. This window will close automatically when done.",
|
|
305
|
+
method: "auto" as const,
|
|
306
|
+
callback: async () => {
|
|
307
|
+
try {
|
|
308
|
+
return { type: "success" as const, key: await tokenPromise }
|
|
309
|
+
} catch {
|
|
310
|
+
return { type: "failed" as const }
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
}
|
|
307
314
|
} catch {
|
|
308
|
-
return {
|
|
315
|
+
return {
|
|
316
|
+
url: ORYNA_LOGIN_URL,
|
|
317
|
+
instructions: "Failed to start login server. Please use API key instead.",
|
|
318
|
+
method: "code" as const,
|
|
319
|
+
callback: async () => ({ type: "failed" as const }),
|
|
320
|
+
}
|
|
309
321
|
}
|
|
310
322
|
},
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
url: ORYNA_LOGIN_URL,
|
|
315
|
-
instructions: "Failed to start login server. Please use API key instead.",
|
|
316
|
-
method: "code" as const,
|
|
317
|
-
callback: async () => ({ type: "failed" as const }),
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
},
|
|
321
|
-
},
|
|
323
|
+
} as const,
|
|
324
|
+
]
|
|
325
|
+
: []),
|
|
322
326
|
{
|
|
323
327
|
type: "api",
|
|
324
328
|
label: "API key",
|
|
@@ -88,6 +88,7 @@ export const SessionPaths = {
|
|
|
88
88
|
remove: `${root}/:sessionID`,
|
|
89
89
|
update: `${root}/:sessionID`,
|
|
90
90
|
fork: `${root}/:sessionID/fork`,
|
|
91
|
+
focus: `${root}/:sessionID/focus`,
|
|
91
92
|
abort: `${root}/:sessionID/abort`,
|
|
92
93
|
share: `${root}/:sessionID/share`,
|
|
93
94
|
init: `${root}/:sessionID/init`,
|
|
@@ -250,6 +251,18 @@ export const SessionApi = HttpApi.make("session")
|
|
|
250
251
|
description: "Create a new session by forking an existing session at a specific message point.",
|
|
251
252
|
}),
|
|
252
253
|
),
|
|
254
|
+
HttpApiEndpoint.post("focus", SessionPaths.focus, {
|
|
255
|
+
params: { sessionID: SessionID },
|
|
256
|
+
query: WorkspaceRoutingQuery,
|
|
257
|
+
success: described(Schema.Boolean, "Session focused"),
|
|
258
|
+
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
|
259
|
+
}).annotateMerge(
|
|
260
|
+
OpenApi.annotations({
|
|
261
|
+
identifier: "session.focus",
|
|
262
|
+
summary: "Focus session",
|
|
263
|
+
description: "Navigate the TUI to the specified session via SSE event.",
|
|
264
|
+
}),
|
|
265
|
+
),
|
|
253
266
|
HttpApiEndpoint.post("abort", SessionPaths.abort, {
|
|
254
267
|
params: { sessionID: SessionID },
|
|
255
268
|
query: WorkspaceRoutingQuery,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
|
|
2
2
|
import { Agent } from "@/agent/agent"
|
|
3
|
+
import { GlobalBus } from "@/bus/global"
|
|
3
4
|
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
|
4
5
|
import { EventV2Bridge } from "@/event-v2-bridge"
|
|
5
6
|
import { Command } from "@/command"
|
|
@@ -410,6 +411,22 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
|
|
410
411
|
return yield* session.updatePart(payload)
|
|
411
412
|
})
|
|
412
413
|
|
|
414
|
+
const focus = Effect.fn("SessionHttpApi.focus")(function* (ctx: {
|
|
415
|
+
params: { sessionID: SessionID }
|
|
416
|
+
}) {
|
|
417
|
+
const info = yield* requireSession(ctx.params.sessionID)
|
|
418
|
+
yield* Effect.sync(() => {
|
|
419
|
+
GlobalBus.emit("event", {
|
|
420
|
+
directory: info.directory,
|
|
421
|
+
payload: {
|
|
422
|
+
type: "tui.session.select",
|
|
423
|
+
properties: { sessionID: ctx.params.sessionID },
|
|
424
|
+
},
|
|
425
|
+
})
|
|
426
|
+
})
|
|
427
|
+
return true
|
|
428
|
+
})
|
|
429
|
+
|
|
413
430
|
return handlers
|
|
414
431
|
.handle("list", list)
|
|
415
432
|
.handle("status", status)
|
|
@@ -423,6 +440,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
|
|
423
440
|
.handle("remove", remove)
|
|
424
441
|
.handle("update", update)
|
|
425
442
|
.handleRaw("fork", forkRaw)
|
|
443
|
+
.handle("focus", focus)
|
|
426
444
|
.handle("abort", abort)
|
|
427
445
|
.handle("init", init)
|
|
428
446
|
.handle("share", share)
|
package/src/session/processor.ts
CHANGED
|
@@ -917,7 +917,7 @@ export const layer = Layer.effect(
|
|
|
917
917
|
|
|
918
918
|
const msgs = yield* session.messages({ sessionID: ctx.sessionID }).pipe(Effect.orDie)
|
|
919
919
|
const lastUser = msgs.findLast((m) => m.info.role === "user")
|
|
920
|
-
const collabMatch = (lastUser?.info as any)?.system?.match(/
|
|
920
|
+
const collabMatch = (lastUser?.info as any)?.system?.match(/collab:from=(\S+)/)
|
|
921
921
|
if (collabMatch) {
|
|
922
922
|
const userParts = lastUser?.parts ?? []
|
|
923
923
|
const alreadyReplied = userParts.some((p) => p.type === "text" && (p as any).text?.includes("✓ Replied"))
|