@kidsinai/kids-client 0.0.19 → 0.0.20
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 +2 -2
- package/src/core/debug.ts +51 -0
- package/src/core/events.ts +22 -0
- package/src/core/session.ts +56 -10
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@kidsinai/kids-client",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.20",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Own-client TUI for Kids OpenCode — talks to local `opencode serve` via @opencode-ai/sdk v2 with kid-warm rendering, mission progress, permission dialog, and stderr-tail audit pipeline.",
|
|
7
7
|
"license": "MIT",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"ink-spinner": "^5.0.0",
|
|
36
36
|
"ink-text-input": "^6.0.0",
|
|
37
37
|
"react": "^18.3.1",
|
|
38
|
-
"@kidsinai/kids-opencode-plugin": "^0.0.
|
|
38
|
+
"@kidsinai/kids-opencode-plugin": "^0.0.20"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@opencode-ai/sdk": "^1.14.51",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny file-based debug logger.
|
|
3
|
+
*
|
|
4
|
+
* Always-on writes to ~/.config/kids-opencode/debug.log so we can see what
|
|
5
|
+
* actually happened in a TUI session after the fact (Ink owns the terminal,
|
|
6
|
+
* so console.log/error doesn't show on screen and would also break the
|
|
7
|
+
* alt-screen-buffer canvas).
|
|
8
|
+
*
|
|
9
|
+
* Set KIDS_DEBUG=0 to silence it. Otherwise it's on by default while we're
|
|
10
|
+
* still chasing the "thinking…" hang in dogfood.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { appendFileSync, mkdirSync } from "node:fs"
|
|
14
|
+
import { dirname, join } from "node:path"
|
|
15
|
+
import { homedir } from "node:os"
|
|
16
|
+
|
|
17
|
+
const PATH = process.env.KIDS_DEBUG_LOG
|
|
18
|
+
?? join(process.env.KIDS_OPENCODE_CONFIG_DIR ?? join(homedir(), ".config", "kids-opencode"), "debug.log")
|
|
19
|
+
|
|
20
|
+
const ENABLED = process.env.KIDS_DEBUG !== "0"
|
|
21
|
+
|
|
22
|
+
let initialised = false
|
|
23
|
+
|
|
24
|
+
function init(): void {
|
|
25
|
+
if (initialised) return
|
|
26
|
+
try {
|
|
27
|
+
mkdirSync(dirname(PATH), { recursive: true })
|
|
28
|
+
appendFileSync(PATH, `\n===== kids-client debug log opened at ${new Date().toISOString()} (pid ${process.pid}) =====\n`, "utf8")
|
|
29
|
+
} catch {
|
|
30
|
+
// can't init — disable to avoid further failures
|
|
31
|
+
initialised = true
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
initialised = true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function debug(message: string, fields?: Record<string, unknown>): void {
|
|
38
|
+
if (!ENABLED) return
|
|
39
|
+
init()
|
|
40
|
+
try {
|
|
41
|
+
const stamp = new Date().toISOString()
|
|
42
|
+
const tail = fields ? " " + JSON.stringify(fields) : ""
|
|
43
|
+
appendFileSync(PATH, `${stamp} ${message}${tail}\n`, "utf8")
|
|
44
|
+
} catch {
|
|
45
|
+
// best-effort, never throw from the logger
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function debugLogPath(): string {
|
|
50
|
+
return PATH
|
|
51
|
+
}
|
package/src/core/events.ts
CHANGED
|
@@ -66,6 +66,14 @@ export class EventSubscriber {
|
|
|
66
66
|
this.abort.abort()
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
private dbg(message: string, fields?: Record<string, unknown>): void {
|
|
70
|
+
// Lazy require to avoid a hard dep cycle if debug.ts grows imports.
|
|
71
|
+
try {
|
|
72
|
+
const { debug } = require("./debug.ts") as { debug: (m: string, f?: Record<string, unknown>) => void }
|
|
73
|
+
debug(`[events] ${message}`, fields)
|
|
74
|
+
} catch { /* never let logging block events */ }
|
|
75
|
+
}
|
|
76
|
+
|
|
69
77
|
private async consume(): Promise<void> {
|
|
70
78
|
// The SDK exposes the SSE stream via client.global.event(). The shape
|
|
71
79
|
// has changed across SDK versions:
|
|
@@ -78,19 +86,24 @@ export class EventSubscriber {
|
|
|
78
86
|
if (!eventApi || typeof eventApi.event !== "function") {
|
|
79
87
|
throw new Error("@opencode-ai/sdk/v2: client.global.event() not available — SDK version drift")
|
|
80
88
|
}
|
|
89
|
+
this.dbg("consume: calling event()")
|
|
81
90
|
const result = await Promise.resolve(eventApi.event())
|
|
91
|
+
this.dbg("consume: event() returned", { shape: describeShape(result) })
|
|
82
92
|
const iterable = pickAsyncIterable(result)
|
|
83
93
|
if (!iterable) {
|
|
84
94
|
throw new Error(`@opencode-ai/sdk/v2: client.global.event() returned an unrecognised shape: ${describeShape(result)}`)
|
|
85
95
|
}
|
|
96
|
+
this.dbg("consume: got iterable, awaiting SSE events…")
|
|
86
97
|
for await (const raw of iterable) {
|
|
87
98
|
if (this.abort.signal.aborted) return
|
|
88
99
|
if (this.retries > 0) {
|
|
89
100
|
this.retries = 0
|
|
90
101
|
this.handlers.onReconnected?.()
|
|
91
102
|
}
|
|
103
|
+
this.dbg("consume: raw event", { preview: previewRaw(raw) })
|
|
92
104
|
this.dispatch(raw)
|
|
93
105
|
}
|
|
106
|
+
this.dbg("consume: iterable ended")
|
|
94
107
|
}
|
|
95
108
|
|
|
96
109
|
private dispatch(raw: unknown): void {
|
|
@@ -239,3 +252,12 @@ function describeShape(value: unknown): string {
|
|
|
239
252
|
if (typeof value !== "object") return typeof value
|
|
240
253
|
return `object keys=[${Object.keys(value as object).join(",")}]`
|
|
241
254
|
}
|
|
255
|
+
|
|
256
|
+
function previewRaw(raw: unknown): string {
|
|
257
|
+
try {
|
|
258
|
+
const s = JSON.stringify(raw)
|
|
259
|
+
return s.length > 220 ? s.slice(0, 220) + "…" : s
|
|
260
|
+
} catch {
|
|
261
|
+
return describeShape(raw)
|
|
262
|
+
}
|
|
263
|
+
}
|
package/src/core/session.ts
CHANGED
|
@@ -11,6 +11,16 @@
|
|
|
11
11
|
|
|
12
12
|
import type { OpencodeClient } from "./connection.ts"
|
|
13
13
|
import type { SessionSummary } from "./store.ts"
|
|
14
|
+
import { debug } from "./debug.ts"
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* SDK 1.14.x default is `ThrowOnError=false` which returns the discriminated
|
|
18
|
+
* union { data, error }. If we don't inspect `.error`, auth / 4xx / 5xx
|
|
19
|
+
* responses are silently swallowed and the kid sees "thinking…" forever.
|
|
20
|
+
* Pass this options object to every call so the SDK throws and the
|
|
21
|
+
* orchestrator's existing try/catch can surface the error on ErrorScreen.
|
|
22
|
+
*/
|
|
23
|
+
const SDK_THROW: { throwOnError: true } = { throwOnError: true }
|
|
14
24
|
|
|
15
25
|
export class SessionManager {
|
|
16
26
|
private client: OpencodeClient
|
|
@@ -50,35 +60,59 @@ export class SessionManager {
|
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
async create(): Promise<string> {
|
|
53
|
-
const api = (this.client as unknown as { session?: { create: (input?: unknown) => Promise<{ data?: { id?: string } } | { id?: string } | string> } }).session
|
|
63
|
+
const api = (this.client as unknown as { session?: { create: (input?: unknown, options?: unknown) => Promise<{ data?: { id?: string } } | { id?: string } | string> } }).session
|
|
54
64
|
if (!api?.create) throw new Error("SDK v2: client.session.create unavailable")
|
|
55
|
-
|
|
65
|
+
debug("session.create: calling SDK")
|
|
66
|
+
const result = await api.create({}, SDK_THROW)
|
|
67
|
+
debug("session.create: result shape", { keys: result && typeof result === "object" ? Object.keys(result) : typeof result })
|
|
56
68
|
const id = extractId(result)
|
|
57
69
|
if (!id) throw new Error("SDK v2 session.create returned no id")
|
|
58
70
|
this.currentSessionId = id
|
|
71
|
+
debug("session.create: id", { id })
|
|
59
72
|
return id
|
|
60
73
|
}
|
|
61
74
|
|
|
62
75
|
async prompt(text: string, opts?: { model?: string; agent?: string }): Promise<void> {
|
|
63
76
|
if (!this.currentSessionId) await this.create()
|
|
64
77
|
const sessionID = this.currentSessionId!
|
|
65
|
-
const api = (this.client as unknown as { session?: { prompt: (
|
|
78
|
+
const api = (this.client as unknown as { session?: { prompt: (parameters: unknown, options?: unknown) => Promise<unknown> } }).session
|
|
66
79
|
if (!api?.prompt) throw new Error("SDK v2: client.session.prompt unavailable")
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
80
|
+
// SDK 1.14.51 signature: single parameters object, kid's text under .prompt.text.
|
|
81
|
+
// Pass SDK_THROW so 4xx/5xx surface as exceptions instead of getting
|
|
82
|
+
// silently swallowed (the bug behind the "thinking…" hang).
|
|
83
|
+
// `model` (from the /model picker) is a "providerID/modelID" string; the SDK
|
|
84
|
+
// wants it split into { providerID, modelID }.
|
|
85
|
+
const model = splitModelId(opts?.model)
|
|
86
|
+
const payload = {
|
|
87
|
+
sessionID,
|
|
88
|
+
prompt: { text },
|
|
89
|
+
...(model ? { model } : {}),
|
|
90
|
+
...(opts?.agent ? { agent: opts.agent } : {}),
|
|
91
|
+
}
|
|
92
|
+
debug("session.prompt: sending", { sessionID, textLen: text.length })
|
|
93
|
+
try {
|
|
94
|
+
const result = await api.prompt(payload, SDK_THROW)
|
|
95
|
+
debug("session.prompt: SDK returned", { keys: result && typeof result === "object" ? Object.keys(result) : typeof result })
|
|
96
|
+
} catch (err) {
|
|
97
|
+
debug("session.prompt: SDK threw", { error: errMsg(err) })
|
|
98
|
+
throw err
|
|
99
|
+
}
|
|
72
100
|
}
|
|
73
101
|
|
|
74
102
|
async abort(): Promise<void> {
|
|
75
103
|
if (!this.currentSessionId) return
|
|
76
|
-
const api = (this.client as unknown as { session?: { abort: (
|
|
104
|
+
const api = (this.client as unknown as { session?: { abort: (parameters: unknown, options?: unknown) => Promise<unknown> } }).session
|
|
77
105
|
if (!api?.abort) return
|
|
78
|
-
|
|
106
|
+
debug("session.abort", { sessionID: this.currentSessionId })
|
|
107
|
+
await api.abort({ sessionID: this.currentSessionId }, SDK_THROW)
|
|
79
108
|
}
|
|
80
109
|
}
|
|
81
110
|
|
|
111
|
+
function errMsg(err: unknown): string {
|
|
112
|
+
if (err instanceof Error) return `${err.name}: ${err.message}`
|
|
113
|
+
try { return JSON.stringify(err) } catch { return String(err) }
|
|
114
|
+
}
|
|
115
|
+
|
|
82
116
|
function extractId(result: unknown): string | null {
|
|
83
117
|
if (typeof result === "string") return result
|
|
84
118
|
if (result && typeof result === "object") {
|
|
@@ -88,6 +122,18 @@ function extractId(result: unknown): string | null {
|
|
|
88
122
|
return null
|
|
89
123
|
}
|
|
90
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Split a "providerID/modelID" id (as built by the /model picker) into the
|
|
127
|
+
* shape the SDK prompt body wants. Splits on the FIRST slash so model ids that
|
|
128
|
+
* themselves contain "/" survive. Returns undefined for empty/no input.
|
|
129
|
+
*/
|
|
130
|
+
function splitModelId(id: string | undefined): { providerID: string; modelID: string } | undefined {
|
|
131
|
+
if (!id) return undefined
|
|
132
|
+
const slash = id.indexOf("/")
|
|
133
|
+
if (slash <= 0 || slash === id.length - 1) return undefined
|
|
134
|
+
return { providerID: id.slice(0, slash), modelID: id.slice(slash + 1) }
|
|
135
|
+
}
|
|
136
|
+
|
|
91
137
|
/** SDK list responses come back as `T[]` or `{ data: T[] }` across versions. */
|
|
92
138
|
function unwrapArray(result: unknown): unknown[] {
|
|
93
139
|
if (Array.isArray(result)) return result
|