typeclaw 0.10.0 → 0.11.1
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 +5 -1
- package/package.json +1 -1
- package/src/agent/index.ts +37 -4
- package/src/agent/multimodal/look-at.ts +8 -0
- package/src/agent/restart-handoff/index.ts +91 -0
- package/src/agent/restart-handoff/paths.ts +11 -0
- package/src/agent/session-origin.ts +30 -10
- package/src/agent/subagent-completion-reminder.ts +4 -2
- package/src/agent/system-prompt.ts +3 -1
- package/src/agent/tools/restart.ts +42 -1
- package/src/agent/tools/skip-response.ts +157 -0
- package/src/bundled-plugins/memory/README.md +18 -2
- package/src/bundled-plugins/memory/index.ts +108 -6
- package/src/bundled-plugins/memory/memory-logger.ts +33 -24
- package/src/bundled-plugins/security/policies/prompt-injection.ts +1 -1
- package/src/channels/adapters/discord-bot-invite.ts +89 -0
- package/src/channels/adapters/github/auth-app.ts +53 -9
- package/src/channels/adapters/github/auth-pat.ts +4 -1
- package/src/channels/adapters/github/auth.ts +10 -0
- package/src/channels/adapters/github/event-permissions.ts +83 -0
- package/src/channels/adapters/github/inbound.ts +126 -1
- package/src/channels/adapters/github/index.ts +60 -66
- package/src/channels/adapters/github/outbound.ts +65 -17
- package/src/channels/adapters/github/permission-guidance.ts +169 -0
- package/src/channels/adapters/github/team-membership.ts +56 -0
- package/src/channels/adapters/kakaotalk-classify.ts +13 -1
- package/src/channels/adapters/kakaotalk.ts +2 -0
- package/src/channels/router.ts +269 -34
- package/src/channels/schema.ts +8 -7
- package/src/channels/types.ts +1 -1
- package/src/cli/channel.ts +138 -52
- package/src/cli/init.ts +139 -100
- package/src/cli/inspect-controller.ts +66 -0
- package/src/cli/inspect.ts +24 -32
- package/src/cli/prompt-pem.ts +113 -0
- package/src/cli/run.ts +24 -5
- package/src/cli/tui.ts +34 -10
- package/src/cli/tunnel.ts +453 -14
- package/src/cli/ui.ts +22 -0
- package/src/compose/discover.ts +5 -0
- package/src/config/config.ts +35 -7
- package/src/config/providers.ts +64 -56
- package/src/init/env-file.ts +66 -0
- package/src/init/hatching.ts +32 -5
- package/src/init/index.ts +131 -39
- package/src/init/validate-api-key.ts +31 -0
- package/src/inspect/index.ts +5 -1
- package/src/inspect/loop.ts +12 -1
- package/src/inspect/replay.ts +15 -1
- package/src/run/codex-fetch-observer.ts +377 -0
- package/src/run/index.ts +14 -2
- package/src/server/command-runner.ts +31 -2
- package/src/server/index.ts +59 -1
- package/src/shared/protocol.ts +1 -1
- package/src/skills/typeclaw-channel-github/SKILL.md +47 -1
- package/src/skills/typeclaw-tunnels/SKILL.md +33 -1
- package/src/tui/index.ts +17 -5
- package/src/tunnels/index.ts +1 -0
- package/src/tunnels/manager.ts +18 -0
- package/src/tunnels/providers/cloudflare-named.ts +224 -0
- package/src/tunnels/types.ts +17 -1
- package/typeclaw.schema.json +25 -7
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { createInterface, type Interface } from 'node:readline'
|
|
3
|
+
|
|
4
|
+
import { log } from '@clack/prompts'
|
|
5
|
+
|
|
6
|
+
const BEGIN_MARKER = '-----BEGIN'
|
|
7
|
+
const END_MARKER_RE = /^-----END [A-Z0-9 ]*PRIVATE KEY-----\s*$/
|
|
8
|
+
const END_MARKER_INLINE_RE = /-----END [A-Z0-9 ]*PRIVATE KEY-----/
|
|
9
|
+
|
|
10
|
+
export const CANCEL_SYMBOL = Symbol('cancel')
|
|
11
|
+
|
|
12
|
+
export type ReadLineFn = () => Promise<string | typeof CANCEL_SYMBOL>
|
|
13
|
+
|
|
14
|
+
export async function promptPrivateKeyPem(message: string): Promise<string | typeof CANCEL_SYMBOL> {
|
|
15
|
+
log.step(message)
|
|
16
|
+
log.message('Paste the PEM (including BEGIN/END lines), a path to a .pem file, or an escaped PEM.')
|
|
17
|
+
|
|
18
|
+
const reader = createStdinLineReader()
|
|
19
|
+
try {
|
|
20
|
+
const raw = await readPrivateKeyFromLines(reader.next)
|
|
21
|
+
if (raw === CANCEL_SYMBOL) return CANCEL_SYMBOL
|
|
22
|
+
return await resolvePrivateKeyInput(raw)
|
|
23
|
+
} finally {
|
|
24
|
+
reader.close()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read a PEM block (or single-line value) using `readLine`.
|
|
30
|
+
*
|
|
31
|
+
* A line starting with `-----BEGIN` switches into block mode, accumulating
|
|
32
|
+
* until a line matches `-----END ... PRIVATE KEY-----`. Otherwise the first
|
|
33
|
+
* non-empty line is returned verbatim (path or escaped PEM). Leading blank
|
|
34
|
+
* lines are skipped so a stray Enter does not abort the prompt.
|
|
35
|
+
*/
|
|
36
|
+
export async function readPrivateKeyFromLines(readLine: ReadLineFn): Promise<string | typeof CANCEL_SYMBOL> {
|
|
37
|
+
let first: string
|
|
38
|
+
while (true) {
|
|
39
|
+
const line = await readLine()
|
|
40
|
+
if (line === CANCEL_SYMBOL) return CANCEL_SYMBOL
|
|
41
|
+
if (line.trim().length > 0) {
|
|
42
|
+
first = line
|
|
43
|
+
break
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!first.trimStart().startsWith(BEGIN_MARKER)) return first.trim()
|
|
48
|
+
|
|
49
|
+
// Escaped-PEM pasted as one line (contains both BEGIN and END markers and
|
|
50
|
+
// no real newlines) bypasses block mode entirely.
|
|
51
|
+
if (END_MARKER_INLINE_RE.test(first)) return first.trim()
|
|
52
|
+
|
|
53
|
+
const lines: string[] = [first.trimEnd()]
|
|
54
|
+
while (true) {
|
|
55
|
+
const line = await readLine()
|
|
56
|
+
if (line === CANCEL_SYMBOL) return CANCEL_SYMBOL
|
|
57
|
+
const trimmed = line.trimEnd()
|
|
58
|
+
lines.push(trimmed)
|
|
59
|
+
if (END_MARKER_RE.test(trimmed)) break
|
|
60
|
+
}
|
|
61
|
+
return `${lines.join('\n')}\n`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function resolvePrivateKeyInput(input: string): Promise<string> {
|
|
65
|
+
const unescaped = input.includes('\\n') && !input.includes('\n') ? input.replace(/\\n/g, '\n') : input
|
|
66
|
+
if (unescaped.includes('-----BEGIN') && unescaped.includes('PRIVATE KEY-----')) return unescaped
|
|
67
|
+
return await readFile(input, 'utf8')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type StdinLineReader = {
|
|
71
|
+
next: ReadLineFn
|
|
72
|
+
close: () => void
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function createStdinLineReader(): StdinLineReader {
|
|
76
|
+
return createReadlineLineReader(process.stdin)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createReadlineLineReader(input: NodeJS.ReadableStream): StdinLineReader {
|
|
80
|
+
const rl: Interface = createInterface({ input, terminal: false })
|
|
81
|
+
const queue: string[] = []
|
|
82
|
+
const waiters: ((value: string | typeof CANCEL_SYMBOL) => void)[] = []
|
|
83
|
+
let closed = false
|
|
84
|
+
|
|
85
|
+
rl.on('line', (line) => {
|
|
86
|
+
const waiter = waiters.shift()
|
|
87
|
+
if (waiter) waiter(line)
|
|
88
|
+
else queue.push(line)
|
|
89
|
+
})
|
|
90
|
+
rl.on('close', () => {
|
|
91
|
+
closed = true
|
|
92
|
+
for (const w of waiters.splice(0)) w(CANCEL_SYMBOL)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const next: ReadLineFn = () =>
|
|
96
|
+
new Promise((resolve) => {
|
|
97
|
+
const queued = queue.shift()
|
|
98
|
+
if (queued !== undefined) {
|
|
99
|
+
resolve(queued)
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
if (closed) {
|
|
103
|
+
resolve(CANCEL_SYMBOL)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
waiters.push(resolve)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
next,
|
|
111
|
+
close: () => rl.close(),
|
|
112
|
+
}
|
|
113
|
+
}
|
package/src/cli/run.ts
CHANGED
|
@@ -49,21 +49,40 @@ export const run = defineCommand({
|
|
|
49
49
|
initialPrompt: args.prompt,
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
const exit = (code: number): void => {
|
|
53
|
+
process.exit(code)
|
|
54
|
+
}
|
|
55
|
+
const onSignal = (): void => {
|
|
56
|
+
void shutdown({ stop, exit })
|
|
55
57
|
}
|
|
56
58
|
process.once('SIGINT', onSignal)
|
|
57
59
|
process.once('SIGTERM', onSignal)
|
|
58
60
|
|
|
59
61
|
if (tuiPromise) {
|
|
60
62
|
await tuiPromise
|
|
61
|
-
stop
|
|
62
|
-
process.exit(0)
|
|
63
|
+
await shutdown({ stop, exit })
|
|
63
64
|
}
|
|
64
65
|
},
|
|
65
66
|
})
|
|
66
67
|
|
|
68
|
+
// Awaits `stop()` BEFORE exiting so async teardown side-effects (channel
|
|
69
|
+
// adapter teardown, in particular GitHub webhook deregistration) actually
|
|
70
|
+
// complete. The previous code called `stop()` without awaiting and then
|
|
71
|
+
// `process.exit(0)` synchronously, so the in-process DELETE /repos/.../hooks/
|
|
72
|
+
// requests never went out and webhooks survived `typeclaw stop` (which
|
|
73
|
+
// `docker stop`s the container → SIGTERM).
|
|
74
|
+
export async function shutdown(deps: {
|
|
75
|
+
stop: () => void | Promise<void>
|
|
76
|
+
exit: (code: number) => void
|
|
77
|
+
}): Promise<void> {
|
|
78
|
+
try {
|
|
79
|
+
await deps.stop()
|
|
80
|
+
deps.exit(0)
|
|
81
|
+
} catch {
|
|
82
|
+
deps.exit(1)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
67
86
|
function resolveAttachTui({
|
|
68
87
|
tui,
|
|
69
88
|
noTui,
|
package/src/cli/tui.ts
CHANGED
|
@@ -25,16 +25,40 @@ export const tui = defineCommand({
|
|
|
25
25
|
},
|
|
26
26
|
},
|
|
27
27
|
async run({ args }) {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
const resolveUrl: () => Promise<string> = args.url !== undefined ? async () => args.url as string : defaultUrl
|
|
29
|
+
|
|
30
|
+
let initialPrompt: string | undefined = args.prompt
|
|
31
|
+
let attempt = 0
|
|
32
|
+
const RECONNECT_MAX_ATTEMPTS = 30
|
|
33
|
+
const RECONNECT_BACKOFF_MS = 1_000
|
|
34
|
+
|
|
35
|
+
while (true) {
|
|
36
|
+
const url = await resolveUrl()
|
|
37
|
+
const tui = createTui({
|
|
38
|
+
url,
|
|
39
|
+
...(initialPrompt !== undefined ? { initialPrompt } : {}),
|
|
40
|
+
expectedVersion: CLI_VERSION,
|
|
41
|
+
onVersionMismatch: (info) => {
|
|
42
|
+
process.stderr.write(`${formatVersionMismatchWarning(info)}\n`)
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
const outcome = await tui.run()
|
|
46
|
+
if (!outcome.lostConnection) return
|
|
47
|
+
// The TUI lost its WS post-handshake (container restart, network blip,
|
|
48
|
+
// hostd hiccup). Re-resolve the URL because the host port can change
|
|
49
|
+
// across container lifecycles (see resolveHostPort), then reconnect.
|
|
50
|
+
// The initial prompt is intentionally cleared after the first cycle:
|
|
51
|
+
// on a reconnect, the agent is resuming the same session — replaying
|
|
52
|
+
// the prompt would re-send it to the LLM.
|
|
53
|
+
initialPrompt = undefined
|
|
54
|
+
attempt += 1
|
|
55
|
+
if (attempt > RECONNECT_MAX_ATTEMPTS) {
|
|
56
|
+
console.error(errorLine(`disconnected; gave up after ${RECONNECT_MAX_ATTEMPTS} reconnect attempts`))
|
|
57
|
+
process.exit(1)
|
|
58
|
+
}
|
|
59
|
+
process.stderr.write(`reconnecting (attempt ${attempt}/${RECONNECT_MAX_ATTEMPTS})...\n`)
|
|
60
|
+
await new Promise((resolve) => setTimeout(resolve, RECONNECT_BACKOFF_MS))
|
|
61
|
+
}
|
|
38
62
|
},
|
|
39
63
|
})
|
|
40
64
|
|