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.
Files changed (62) hide show
  1. package/README.md +5 -1
  2. package/package.json +1 -1
  3. package/src/agent/index.ts +37 -4
  4. package/src/agent/multimodal/look-at.ts +8 -0
  5. package/src/agent/restart-handoff/index.ts +91 -0
  6. package/src/agent/restart-handoff/paths.ts +11 -0
  7. package/src/agent/session-origin.ts +30 -10
  8. package/src/agent/subagent-completion-reminder.ts +4 -2
  9. package/src/agent/system-prompt.ts +3 -1
  10. package/src/agent/tools/restart.ts +42 -1
  11. package/src/agent/tools/skip-response.ts +157 -0
  12. package/src/bundled-plugins/memory/README.md +18 -2
  13. package/src/bundled-plugins/memory/index.ts +108 -6
  14. package/src/bundled-plugins/memory/memory-logger.ts +33 -24
  15. package/src/bundled-plugins/security/policies/prompt-injection.ts +1 -1
  16. package/src/channels/adapters/discord-bot-invite.ts +89 -0
  17. package/src/channels/adapters/github/auth-app.ts +53 -9
  18. package/src/channels/adapters/github/auth-pat.ts +4 -1
  19. package/src/channels/adapters/github/auth.ts +10 -0
  20. package/src/channels/adapters/github/event-permissions.ts +83 -0
  21. package/src/channels/adapters/github/inbound.ts +126 -1
  22. package/src/channels/adapters/github/index.ts +60 -66
  23. package/src/channels/adapters/github/outbound.ts +65 -17
  24. package/src/channels/adapters/github/permission-guidance.ts +169 -0
  25. package/src/channels/adapters/github/team-membership.ts +56 -0
  26. package/src/channels/adapters/kakaotalk-classify.ts +13 -1
  27. package/src/channels/adapters/kakaotalk.ts +2 -0
  28. package/src/channels/router.ts +269 -34
  29. package/src/channels/schema.ts +8 -7
  30. package/src/channels/types.ts +1 -1
  31. package/src/cli/channel.ts +138 -52
  32. package/src/cli/init.ts +139 -100
  33. package/src/cli/inspect-controller.ts +66 -0
  34. package/src/cli/inspect.ts +24 -32
  35. package/src/cli/prompt-pem.ts +113 -0
  36. package/src/cli/run.ts +24 -5
  37. package/src/cli/tui.ts +34 -10
  38. package/src/cli/tunnel.ts +453 -14
  39. package/src/cli/ui.ts +22 -0
  40. package/src/compose/discover.ts +5 -0
  41. package/src/config/config.ts +35 -7
  42. package/src/config/providers.ts +64 -56
  43. package/src/init/env-file.ts +66 -0
  44. package/src/init/hatching.ts +32 -5
  45. package/src/init/index.ts +131 -39
  46. package/src/init/validate-api-key.ts +31 -0
  47. package/src/inspect/index.ts +5 -1
  48. package/src/inspect/loop.ts +12 -1
  49. package/src/inspect/replay.ts +15 -1
  50. package/src/run/codex-fetch-observer.ts +377 -0
  51. package/src/run/index.ts +14 -2
  52. package/src/server/command-runner.ts +31 -2
  53. package/src/server/index.ts +59 -1
  54. package/src/shared/protocol.ts +1 -1
  55. package/src/skills/typeclaw-channel-github/SKILL.md +47 -1
  56. package/src/skills/typeclaw-tunnels/SKILL.md +33 -1
  57. package/src/tui/index.ts +17 -5
  58. package/src/tunnels/index.ts +1 -0
  59. package/src/tunnels/manager.ts +18 -0
  60. package/src/tunnels/providers/cloudflare-named.ts +224 -0
  61. package/src/tunnels/types.ts +17 -1
  62. 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 onSignal = () => {
53
- stop()
54
- process.exit(0)
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 url = args.url ?? (await defaultUrl())
29
- const tui = createTui({
30
- url,
31
- ...(args.prompt !== undefined ? { initialPrompt: args.prompt } : {}),
32
- expectedVersion: CLI_VERSION,
33
- onVersionMismatch: (info) => {
34
- process.stderr.write(`${formatVersionMismatchWarning(info)}\n`)
35
- },
36
- })
37
- await tui.run()
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