typeclaw 0.1.4 → 0.1.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/README.md +1 -1
- package/package.json +1 -1
- package/src/bundled-plugins/memory/README.md +8 -8
- package/src/bundled-plugins/memory/dreaming.ts +117 -1
- package/src/cli/init.ts +35 -6
- package/src/cli/reload.ts +6 -3
- package/src/cli/tui.ts +6 -3
- package/src/config/config.ts +115 -16
- package/src/config/index.ts +8 -1
- package/src/container/index.ts +1 -1
- package/src/container/port.ts +10 -0
- package/src/container/start.ts +46 -9
- package/src/doctor/checks.ts +1 -1
- package/src/doctor/plugin-bridge.ts +28 -4
- package/src/init/dockerfile.ts +4 -4
- package/src/init/gitignore.ts +1 -1
- package/src/init/index.ts +31 -24
- package/src/reload/client.ts +25 -1
- package/src/run/index.ts +13 -1
- package/src/secrets/storage.ts +15 -0
- package/src/server/index.ts +80 -64
- package/src/skills/typeclaw-config/SKILL.md +70 -52
- package/src/skills/typeclaw-memory/SKILL.md +8 -8
- package/src/tui/client.ts +66 -5
- package/src/tui/index.ts +61 -9
- package/typeclaw.schema.json +77 -53
package/src/tui/client.ts
CHANGED
|
@@ -2,9 +2,19 @@ import type { ClientMessage, ServerMessage } from '@/shared'
|
|
|
2
2
|
|
|
3
3
|
export type Client = Awaited<ReturnType<typeof createClient>>
|
|
4
4
|
|
|
5
|
-
export
|
|
5
|
+
export type CreateClientOptions = {
|
|
6
|
+
timeoutMs?: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 30_000
|
|
10
|
+
|
|
11
|
+
export async function createClient(url: string, { timeoutMs = DEFAULT_CONNECT_TIMEOUT_MS }: CreateClientOptions = {}) {
|
|
6
12
|
const ws = new WebSocket(url)
|
|
13
|
+
const displayUrl = redactUrl(url)
|
|
7
14
|
const listeners = new Set<(msg: ServerMessage) => void>()
|
|
15
|
+
const closeListeners = new Set<() => void>()
|
|
16
|
+
const errorListeners = new Set<(err: unknown) => void>()
|
|
17
|
+
let closed = false
|
|
8
18
|
// Buffer messages that arrive before any listener is registered. In-process
|
|
9
19
|
// connections (typeclaw run's local tui) deliver the first server frame
|
|
10
20
|
// before the caller has a chance to attach onMessage.
|
|
@@ -20,12 +30,43 @@ export async function createClient(url: string) {
|
|
|
20
30
|
})
|
|
21
31
|
|
|
22
32
|
ws.addEventListener('close', () => {
|
|
33
|
+
closed = true
|
|
23
34
|
listeners.clear()
|
|
35
|
+
for (const fn of closeListeners) fn()
|
|
36
|
+
closeListeners.clear()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
ws.addEventListener('error', (err) => {
|
|
40
|
+
for (const fn of errorListeners) fn(err)
|
|
24
41
|
})
|
|
25
42
|
|
|
26
43
|
await new Promise<void>((resolve, reject) => {
|
|
27
|
-
|
|
28
|
-
|
|
44
|
+
const timer = setTimeout(() => {
|
|
45
|
+
cleanup()
|
|
46
|
+
ws.close()
|
|
47
|
+
reject(new Error(`timed out connecting to ${displayUrl} after ${timeoutMs}ms`))
|
|
48
|
+
}, timeoutMs)
|
|
49
|
+
const cleanup = () => {
|
|
50
|
+
clearTimeout(timer)
|
|
51
|
+
ws.removeEventListener('open', onOpen)
|
|
52
|
+
ws.removeEventListener('error', onError)
|
|
53
|
+
ws.removeEventListener('close', onClose)
|
|
54
|
+
}
|
|
55
|
+
const onOpen = () => {
|
|
56
|
+
cleanup()
|
|
57
|
+
resolve()
|
|
58
|
+
}
|
|
59
|
+
const onError = (err: unknown) => {
|
|
60
|
+
cleanup()
|
|
61
|
+
reject(err)
|
|
62
|
+
}
|
|
63
|
+
const onClose = () => {
|
|
64
|
+
cleanup()
|
|
65
|
+
reject(new Error(`connection to ${displayUrl} closed before opening`))
|
|
66
|
+
}
|
|
67
|
+
ws.addEventListener('open', onOpen, { once: true })
|
|
68
|
+
ws.addEventListener('error', onError, { once: true })
|
|
69
|
+
ws.addEventListener('close', onClose, { once: true })
|
|
29
70
|
})
|
|
30
71
|
|
|
31
72
|
return {
|
|
@@ -37,9 +78,29 @@ export async function createClient(url: string) {
|
|
|
37
78
|
}
|
|
38
79
|
return () => listeners.delete(fn)
|
|
39
80
|
},
|
|
40
|
-
onClose: (fn: () => void) =>
|
|
41
|
-
|
|
81
|
+
onClose: (fn: () => void) => {
|
|
82
|
+
if (closed) {
|
|
83
|
+
queueMicrotask(fn)
|
|
84
|
+
return () => {}
|
|
85
|
+
}
|
|
86
|
+
closeListeners.add(fn)
|
|
87
|
+
return () => closeListeners.delete(fn)
|
|
88
|
+
},
|
|
89
|
+
onError: (fn: (err: unknown) => void) => {
|
|
90
|
+
errorListeners.add(fn)
|
|
91
|
+
return () => errorListeners.delete(fn)
|
|
92
|
+
},
|
|
42
93
|
send: (msg: ClientMessage) => ws.send(JSON.stringify(msg)),
|
|
43
94
|
close: () => ws.close(),
|
|
44
95
|
}
|
|
45
96
|
}
|
|
97
|
+
|
|
98
|
+
function redactUrl(url: string): string {
|
|
99
|
+
try {
|
|
100
|
+
const parsed = new URL(url)
|
|
101
|
+
if (parsed.searchParams.has('token')) parsed.searchParams.set('token', '<redacted>')
|
|
102
|
+
return parsed.toString()
|
|
103
|
+
} catch {
|
|
104
|
+
return url
|
|
105
|
+
}
|
|
106
|
+
}
|
package/src/tui/index.ts
CHANGED
|
@@ -7,11 +7,14 @@ import { colors, editorTheme, markdownTheme } from './theme'
|
|
|
7
7
|
export type ClientFactory = (url: string) => Promise<Client>
|
|
8
8
|
export type TerminalFactory = () => Terminal
|
|
9
9
|
|
|
10
|
+
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 30_000
|
|
11
|
+
|
|
10
12
|
export type TuiOptions = {
|
|
11
13
|
url: string
|
|
12
14
|
initialPrompt?: string
|
|
13
15
|
createClient?: ClientFactory
|
|
14
16
|
createTerminal?: TerminalFactory
|
|
17
|
+
handshakeTimeoutMs?: number
|
|
15
18
|
exit?: (code: number) => void
|
|
16
19
|
}
|
|
17
20
|
|
|
@@ -20,13 +23,15 @@ export function createTui({
|
|
|
20
23
|
initialPrompt,
|
|
21
24
|
createClient = createClientDefault,
|
|
22
25
|
createTerminal = () => new ProcessTerminal(),
|
|
26
|
+
handshakeTimeoutMs = DEFAULT_HANDSHAKE_TIMEOUT_MS,
|
|
23
27
|
exit = process.exit.bind(process),
|
|
24
28
|
}: TuiOptions) {
|
|
25
29
|
async function run(): Promise<void> {
|
|
26
30
|
const terminal = createTerminal()
|
|
27
31
|
const tui = new TUI(terminal)
|
|
32
|
+
const displayUrl = redactUrl(url)
|
|
28
33
|
|
|
29
|
-
const status = new Text(colors.dim(`connecting to ${
|
|
34
|
+
const status = new Text(colors.dim(`connecting to ${displayUrl}...`), 0, 0)
|
|
30
35
|
tui.addChild(status)
|
|
31
36
|
tui.start()
|
|
32
37
|
tui.requestRender()
|
|
@@ -39,14 +44,13 @@ export function createTui({
|
|
|
39
44
|
throw err
|
|
40
45
|
})
|
|
41
46
|
|
|
42
|
-
const sessionId = await
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
})
|
|
47
|
+
const sessionId = await waitForConnected(client, displayUrl, handshakeTimeoutMs).catch((err) => {
|
|
48
|
+
status.setText(colors.red(`connection error: ${err instanceof Error ? err.message : String(err)}`))
|
|
49
|
+
tui.requestRender()
|
|
50
|
+
client.close()
|
|
51
|
+
tui.stop()
|
|
52
|
+
exit(1)
|
|
53
|
+
throw err
|
|
50
54
|
})
|
|
51
55
|
status.setText(colors.dim(`session: ${sessionId}`))
|
|
52
56
|
tui.requestRender()
|
|
@@ -223,3 +227,51 @@ export function createTui({
|
|
|
223
227
|
|
|
224
228
|
return { run }
|
|
225
229
|
}
|
|
230
|
+
|
|
231
|
+
function redactUrl(url: string): string {
|
|
232
|
+
try {
|
|
233
|
+
const parsed = new URL(url)
|
|
234
|
+
if (parsed.searchParams.has('token')) parsed.searchParams.set('token', '<redacted>')
|
|
235
|
+
return parsed.toString()
|
|
236
|
+
} catch {
|
|
237
|
+
return url
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function waitForConnected(client: Client, url: string, timeoutMs: number): Promise<string> {
|
|
242
|
+
return await new Promise<string>((resolve, reject) => {
|
|
243
|
+
const timer = setTimeout(() => {
|
|
244
|
+
cleanup()
|
|
245
|
+
reject(new Error(`timed out waiting for connected message from ${url} after ${timeoutMs}ms`))
|
|
246
|
+
}, timeoutMs)
|
|
247
|
+
const cleanupFns: Array<() => void> = []
|
|
248
|
+
const cleanup = () => {
|
|
249
|
+
clearTimeout(timer)
|
|
250
|
+
for (const fn of cleanupFns.splice(0)) fn()
|
|
251
|
+
}
|
|
252
|
+
cleanupFns.push(
|
|
253
|
+
client.onMessage((msg) => {
|
|
254
|
+
if (msg.type === 'connected') {
|
|
255
|
+
cleanup()
|
|
256
|
+
resolve(msg.sessionId)
|
|
257
|
+
}
|
|
258
|
+
if (msg.type === 'error') {
|
|
259
|
+
cleanup()
|
|
260
|
+
reject(new Error(msg.message))
|
|
261
|
+
}
|
|
262
|
+
}),
|
|
263
|
+
)
|
|
264
|
+
cleanupFns.push(
|
|
265
|
+
client.onClose(() => {
|
|
266
|
+
cleanup()
|
|
267
|
+
reject(new Error(`connection to ${url} closed before the session was ready`))
|
|
268
|
+
}),
|
|
269
|
+
)
|
|
270
|
+
cleanupFns.push(
|
|
271
|
+
client.onError((err) => {
|
|
272
|
+
cleanup()
|
|
273
|
+
reject(err instanceof Error ? err : new Error(`connection to ${url} failed`))
|
|
274
|
+
}),
|
|
275
|
+
)
|
|
276
|
+
})
|
|
277
|
+
}
|
package/typeclaw.schema.json
CHANGED
|
@@ -731,76 +731,100 @@
|
|
|
731
731
|
}
|
|
732
732
|
}
|
|
733
733
|
},
|
|
734
|
-
"
|
|
734
|
+
"docker": {
|
|
735
735
|
"default": {
|
|
736
|
-
"
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
736
|
+
"file": {
|
|
737
|
+
"ffmpeg": false,
|
|
738
|
+
"gh": true,
|
|
739
|
+
"python": true,
|
|
740
|
+
"tmux": true,
|
|
741
|
+
"append": []
|
|
742
|
+
}
|
|
741
743
|
},
|
|
742
744
|
"type": "object",
|
|
743
745
|
"properties": {
|
|
744
|
-
"
|
|
745
|
-
"default":
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
746
|
+
"file": {
|
|
747
|
+
"default": {
|
|
748
|
+
"ffmpeg": false,
|
|
749
|
+
"gh": true,
|
|
750
|
+
"python": true,
|
|
751
|
+
"tmux": true,
|
|
752
|
+
"append": []
|
|
753
|
+
},
|
|
754
|
+
"type": "object",
|
|
755
|
+
"properties": {
|
|
756
|
+
"ffmpeg": {
|
|
757
|
+
"default": false,
|
|
758
|
+
"anyOf": [
|
|
759
|
+
{
|
|
760
|
+
"type": "boolean"
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
"type": "string",
|
|
764
|
+
"minLength": 1
|
|
765
|
+
}
|
|
766
|
+
]
|
|
749
767
|
},
|
|
750
|
-
{
|
|
751
|
-
"
|
|
752
|
-
"
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
768
|
+
"gh": {
|
|
769
|
+
"default": true,
|
|
770
|
+
"anyOf": [
|
|
771
|
+
{
|
|
772
|
+
"type": "boolean"
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
"type": "string",
|
|
776
|
+
"minLength": 1
|
|
777
|
+
}
|
|
778
|
+
]
|
|
761
779
|
},
|
|
762
|
-
{
|
|
763
|
-
"
|
|
764
|
-
"minLength": 1
|
|
765
|
-
}
|
|
766
|
-
]
|
|
767
|
-
},
|
|
768
|
-
"python": {
|
|
769
|
-
"default": true,
|
|
770
|
-
"type": "boolean"
|
|
771
|
-
},
|
|
772
|
-
"tmux": {
|
|
773
|
-
"default": true,
|
|
774
|
-
"anyOf": [
|
|
775
|
-
{
|
|
780
|
+
"python": {
|
|
781
|
+
"default": true,
|
|
776
782
|
"type": "boolean"
|
|
777
783
|
},
|
|
778
|
-
{
|
|
779
|
-
"
|
|
780
|
-
"
|
|
784
|
+
"tmux": {
|
|
785
|
+
"default": true,
|
|
786
|
+
"anyOf": [
|
|
787
|
+
{
|
|
788
|
+
"type": "boolean"
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
"type": "string",
|
|
792
|
+
"minLength": 1
|
|
793
|
+
}
|
|
794
|
+
]
|
|
795
|
+
},
|
|
796
|
+
"append": {
|
|
797
|
+
"default": [],
|
|
798
|
+
"type": "array",
|
|
799
|
+
"items": {
|
|
800
|
+
"type": "string"
|
|
801
|
+
}
|
|
781
802
|
}
|
|
782
|
-
]
|
|
783
|
-
},
|
|
784
|
-
"append": {
|
|
785
|
-
"default": [],
|
|
786
|
-
"type": "array",
|
|
787
|
-
"items": {
|
|
788
|
-
"type": "string"
|
|
789
803
|
}
|
|
790
804
|
}
|
|
791
805
|
}
|
|
792
806
|
},
|
|
793
|
-
"
|
|
807
|
+
"git": {
|
|
794
808
|
"default": {
|
|
795
|
-
"
|
|
809
|
+
"ignore": {
|
|
810
|
+
"append": []
|
|
811
|
+
}
|
|
796
812
|
},
|
|
797
813
|
"type": "object",
|
|
798
814
|
"properties": {
|
|
799
|
-
"
|
|
800
|
-
"default":
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
815
|
+
"ignore": {
|
|
816
|
+
"default": {
|
|
817
|
+
"append": []
|
|
818
|
+
},
|
|
819
|
+
"type": "object",
|
|
820
|
+
"properties": {
|
|
821
|
+
"append": {
|
|
822
|
+
"default": [],
|
|
823
|
+
"type": "array",
|
|
824
|
+
"items": {
|
|
825
|
+
"type": "string"
|
|
826
|
+
}
|
|
827
|
+
}
|
|
804
828
|
}
|
|
805
829
|
}
|
|
806
830
|
}
|