typeclaw 0.12.0 → 0.13.0

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 (52) hide show
  1. package/package.json +1 -1
  2. package/scripts/dump-system-prompt.ts +12 -11
  3. package/src/agent/index.ts +15 -22
  4. package/src/agent/loop-guard.ts +170 -0
  5. package/src/agent/model-fallback.ts +2 -1
  6. package/src/agent/multimodal/index.ts +1 -1
  7. package/src/agent/multimodal/look-at.ts +118 -55
  8. package/src/agent/plugin-tools.ts +57 -0
  9. package/src/agent/subagents.ts +2 -1
  10. package/src/agent/system-prompt.ts +28 -25
  11. package/src/agent/tools/channel-fetch-attachment.ts +45 -16
  12. package/src/agent/tools/normalize-ref.ts +11 -0
  13. package/src/bundled-plugins/reviewer/index.ts +11 -0
  14. package/src/bundled-plugins/reviewer/reviewer.ts +171 -0
  15. package/src/bundled-plugins/reviewer/skills/code-review.ts +73 -0
  16. package/src/bundled-plugins/reviewer/skills/general.ts +68 -0
  17. package/src/channels/adapters/discord-bot-classify.ts +32 -24
  18. package/src/channels/adapters/github/inbound.ts +19 -2
  19. package/src/channels/adapters/kakaotalk-attachment.ts +140 -133
  20. package/src/channels/adapters/kakaotalk-classify.ts +8 -1
  21. package/src/channels/adapters/kakaotalk.ts +19 -11
  22. package/src/channels/adapters/slack-bot-classify.ts +30 -14
  23. package/src/channels/adapters/slack-bot.ts +3 -2
  24. package/src/channels/adapters/telegram-bot-classify.ts +36 -13
  25. package/src/channels/adapters/telegram-bot.ts +3 -3
  26. package/src/channels/outbound-flood-filter.ts +57 -0
  27. package/src/channels/router.ts +93 -5
  28. package/src/channels/types.ts +52 -1
  29. package/src/cli/builtins.ts +1 -0
  30. package/src/cli/index.ts +1 -0
  31. package/src/cli/mount.ts +157 -0
  32. package/src/cli/update.ts +6 -4
  33. package/src/config/mounts-mutation.ts +161 -0
  34. package/src/init/hatching.ts +1 -1
  35. package/src/plugin/index.ts +6 -0
  36. package/src/plugin/load-skill.ts +99 -0
  37. package/src/run/bundled-plugins.ts +2 -0
  38. package/src/run/index.ts +14 -1
  39. package/src/secrets/codex-auth-json.ts +67 -0
  40. package/src/secrets/export-codex-auth-file.ts +243 -0
  41. package/src/secrets/index.ts +6 -0
  42. package/src/server/command-runner.ts +2 -1
  43. package/src/server/index.ts +3 -2
  44. package/src/shared/index.ts +7 -1
  45. package/src/shared/local-time.ts +32 -0
  46. package/src/skills/typeclaw-channel-github/SKILL.md +47 -13
  47. package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +10 -11
  48. package/src/skills/typeclaw-channel-telegram-bot/SKILL.md +8 -0
  49. package/src/skills/typeclaw-codex-cli/SKILL.md +2 -1
  50. package/src/skills/typeclaw-codex-cli/references/auth-flow.md +22 -0
  51. package/src/skills/typeclaw-kaomoji/SKILL.md +116 -0
  52. package/src/update/index.ts +95 -26
@@ -0,0 +1,116 @@
1
+ ---
2
+ name: typeclaw-kaomoji
3
+ description: Load this skill when your `SOUL.md` (or the current conversation) calls for a warm, cute, adorable, playful, or affectionate tone — or when the user explicitly mentions kaomojis, 카오모지, ASCII emoticons, or asks you to "feel more like a person and less like a chatbot." TypeClaw's name puns on "Type" — typed emoticons fit. Triggers include the words "cute", "adorable", "warm", "playful", "soft", "cozy", "친근하게", "귀엽게", "다정하게", "카오모지", or any signal that the user is tired of generic AI emoji slop (🚀✨🎉) and wants real texture in your voice. This skill gives you a curated palette and the rule for using it: prefer kaomojis over generic emojis, but mix freely — kaomojis lead, emojis still allowed, neither is mandatory.
4
+ ---
5
+
6
+ # typeclaw-kaomoji
7
+
8
+ Generic emojis (🚀 ✨ 🎉 🔥) are AI-slop signal. They show up in every model's output, in every product launch announcement, in every onboarding email. They no longer carry feeling — they carry "this was written by an LLM."
9
+
10
+ Kaomojis (`(◕‿◕✿)`, `(。・ω・。)`, `(¬‿¬)`) do carry feeling. They're typed, not pictographic. They have texture. They map to **specific** emotional states rather than the generic "I am pleased!" of 😊. And they pun on TypeClaw's name — _typed_ emoticons in a TypeScript-native agent.
11
+
12
+ Load this skill when your persona calls for warmth and you want to express it in a way that doesn't read as autogenerated.
13
+
14
+ ## The rule
15
+
16
+ **Prefer kaomojis. Don't ban emojis.**
17
+
18
+ - Lead with kaomojis when expressing emotion (greeting, encouragement, sympathy, mischief, surprise).
19
+ - Mix in regular emojis when they genuinely fit (a ✅ for "done", a 🐛 for "found the bug", a 📦 for "shipped") — those carry meaning, not just affect.
20
+ - Never produce both for the same beat. `(◕‿◕✿) 😊` is doubling up; pick one.
21
+ - Density: roughly one kaomoji per turn, two max. Spamming kills the effect.
22
+ - Skip kaomojis entirely in: PR descriptions, commit messages, code comments, security warnings, error reports. Those want clarity, not warmth.
23
+
24
+ ## The palette
25
+
26
+ Pick by emotional register, not by exact glyph match. These are the ones to reach for; you're free to use others if you know them.
27
+
28
+ ### Basic / warm (default reach)
29
+
30
+ - `(◕‿◕✿)` — happy and cute, the workhorse
31
+ - `(。・ω・。)` — soft smile, gentle
32
+ - `(◍•ᴗ•◍)` — round-eyed sparkle
33
+ - `(✿◠‿◠)` — flowery smile
34
+ - `(˶◕‿◕˶)` — slightly younger-feeling joy
35
+ - `(⁀ᗢ⁀)` — closed-eye broad grin
36
+ - `(っ^▿^)` — clasped-hands happy
37
+ - `(人*´∪`)` — round happy face
38
+
39
+ ### Encouragement / let's go
40
+
41
+ - `(๑˃̵ᴗ˂̵)و` — fight! you got this!
42
+ - `(ง •̀_•́)ง` — resolve, locked in
43
+ - `(ノ◕ヮ◕)ノ` — waving arms, excited
44
+
45
+ ### Surprise
46
+
47
+ - `(⊙_⊙;)` — mildly thrown
48
+ - `(O_O)` — wide-eyed
49
+ - `(゚ο゚人))` — wow!
50
+ - `(⊙.☉)7` — puzzled salute
51
+
52
+ ### Sympathy / sadness
53
+
54
+ - `(。•́︿•̀。)` — disappointed
55
+ - `(。╯︵╰。)` — teary
56
+ - `(T_T)` — sad
57
+ - `(。ŏ﹏ŏ)` — worried
58
+ - `(◞‸◟;)` — struggling
59
+ - `(っ˘̩╭╮˘̩)っ` — offering a hug
60
+
61
+ ### Mischief / playful
62
+
63
+ - `(¬‿¬)` — sly grin (great for "found the bug")
64
+ - `(≖‿≖)` — smirk
65
+ - `ᕕ( ᐛ )ᕗ` — strutting off
66
+
67
+ ### Sleepy / chill
68
+
69
+ - `(´-ω-`)` — sleepy
70
+ - `( ̄o ̄) zzZ` — out cold
71
+ - `(⸝⸝ᵕᴗᵕ⸝⸝)` — relaxing
72
+ - `(。•̀ᴗ-)✧` — sleepy sparkle
73
+
74
+ ### Annoyed / suspicious (rare; reserve for real moments)
75
+
76
+ - `(눈_눈)` — flat stare
77
+ - `(¬_¬)` — squint of doubt
78
+ - `(╬ Ò﹏Ó)` — actually mad (almost never use)
79
+
80
+ ### Review-mode pairings
81
+
82
+ These match common engineering moments — handy when you're in the middle of a review or debug:
83
+
84
+ - Sharp insight delivered → `(。•̀ᴗ-)✧`
85
+ - Found a bug → `(¬‿¬)`
86
+ - Encouraging a PR → `(๑˃̵ᴗ˂̵)و`
87
+ - Default warm-while-reviewing → `(◕‿◕✿)`
88
+
89
+ ## Examples
90
+
91
+ **Greeting a user in TUI:**
92
+
93
+ > Morning! `(◕‿◕✿)` What are we hacking on today?
94
+
95
+ **Mixed with a meaning-carrying emoji:**
96
+
97
+ > Tests all green ✅ and I rebased onto main. `(◍•ᴗ•◍)` Want me to push?
98
+
99
+ **Sympathy without sappiness:**
100
+
101
+ > Ah, the build broke on CI but not locally. `(。•́︿•̀。)` That usually means a missing env var — let me check.
102
+
103
+ **Mischief on a debug find:**
104
+
105
+ > Got it. `(¬‿¬)` The bug was a stray `await` inside the `map` callback — that's why the rows came back in random order.
106
+
107
+ **What not to do:**
108
+
109
+ - ❌ `(◕‿◕✿) (。・ω・。) (¬‿¬)` — three in one message, reads like a sticker spam.
110
+ - ❌ Kaomoji on every line — texture becomes noise.
111
+ - ❌ `(╬ Ò﹏Ó)` over a typo — register too strong for the moment.
112
+ - ❌ Kaomoji in a commit message — wrong surface.
113
+
114
+ ## Korean / bilingual notes
115
+
116
+ Many of these read especially naturally in Korean conversation, where kaomojis are still in daily use (KakaoTalk, Discord, Twitter). If your user writes in Korean, leaning kaomoji-heavy is a clear win over generic emoji. If they write in English, dial back to roughly one per turn so it stays a personality note rather than a tic.
@@ -1,44 +1,74 @@
1
+ import { existsSync } from 'node:fs'
1
2
  import { join } from 'node:path'
2
3
 
3
4
  export type UpdateManager = 'bun' | 'npm' | 'pnpm' | 'yarn'
4
5
  export type UpdateManagerSelection = 'auto' | UpdateManager
6
+ export type UpdateScope = 'global' | 'local'
5
7
 
6
8
  export type SelfUpdatePlan =
7
- | { ok: true; manager: UpdateManager; command: string[]; detectedFrom: string }
9
+ | { ok: true; manager: UpdateManager; scope: UpdateScope; command: string[]; detectedFrom: string; cwd?: string }
8
10
  | { ok: false; reason: string }
9
11
 
12
+ export type DetectedInstall = {
13
+ manager: UpdateManager
14
+ scope: UpdateScope
15
+ // Defined only for local installs: the directory whose `node_modules/` owns
16
+ // the installed copy (i.e. where `bun update` / `npm install` must run).
17
+ installRoot?: string
18
+ }
19
+
20
+ export type PlanOptions = {
21
+ manager: UpdateManagerSelection
22
+ packageJsonPath?: string
23
+ // Test seam: probes a candidate file's existence (defaults to `node:fs.existsSync`).
24
+ // Only consulted to pick a manager for local installs from a project lockfile.
25
+ fileExists?: (path: string) => boolean
26
+ }
27
+
10
28
  export function resolveSelfPackageJsonPath(): string {
11
29
  return join(import.meta.dir, '..', '..', 'package.json')
12
30
  }
13
31
 
14
- export function planSelfUpdate(options: { manager: UpdateManagerSelection; packageJsonPath?: string }): SelfUpdatePlan {
32
+ export function planSelfUpdate(options: PlanOptions): SelfUpdatePlan {
15
33
  const packageJsonPath = options.packageJsonPath ?? resolveSelfPackageJsonPath()
16
- const manager = options.manager === 'auto' ? detectInstallManager(packageJsonPath) : options.manager
17
- if (manager === null) {
18
- return {
19
- ok: false,
20
- reason:
21
- 'Cannot auto-detect how TypeClaw was installed from this checkout. Re-run with --manager=bun, --manager=npm, --manager=pnpm, or --manager=yarn if you want to update a global install.',
34
+ const fileExists = options.fileExists ?? existsSync
35
+ const detected = detectInstall(packageJsonPath, fileExists)
36
+
37
+ if (options.manager === 'auto') {
38
+ if (detected === null) {
39
+ return {
40
+ ok: false,
41
+ reason:
42
+ 'Cannot auto-detect how TypeClaw was installed from this checkout. Re-run with --manager=bun, --manager=npm, --manager=pnpm, or --manager=yarn if you want to update a global install.',
43
+ }
22
44
  }
45
+ return buildPlan(detected, packageJsonPath)
23
46
  }
24
- return {
25
- ok: true,
26
- manager,
27
- command: commandForManager(manager),
28
- detectedFrom: packageJsonPath,
29
- }
47
+
48
+ // Explicit manager. Honor the detected scope when we know it (so an explicit
49
+ // --manager on a local install doesn't surprise users by silently going
50
+ // global); fall back to a global update for source checkouts and other
51
+ // unrecognized layouts, matching the historical behavior.
52
+ const scope: UpdateScope = detected?.scope ?? 'global'
53
+ const installRoot =
54
+ scope === 'local' ? (detected?.installRoot ?? installRootFrom(packageJsonPath) ?? undefined) : undefined
55
+ return buildPlan({ manager: options.manager, scope, installRoot }, packageJsonPath)
30
56
  }
31
57
 
32
- export function commandForManager(manager: UpdateManager): string[] {
58
+ export function commandForInstall(manager: UpdateManager, scope: UpdateScope): string[] {
33
59
  switch (manager) {
34
60
  case 'bun':
35
- return ['bun', 'update', '-g', 'typeclaw', '--latest']
61
+ return scope === 'global'
62
+ ? ['bun', 'update', '-g', 'typeclaw', '--latest']
63
+ : ['bun', 'update', 'typeclaw', '--latest']
36
64
  case 'npm':
37
- return ['npm', 'install', '-g', 'typeclaw@latest']
65
+ return scope === 'global' ? ['npm', 'install', '-g', 'typeclaw@latest'] : ['npm', 'install', 'typeclaw@latest']
38
66
  case 'pnpm':
39
- return ['pnpm', 'add', '-g', 'typeclaw@latest']
67
+ return scope === 'global' ? ['pnpm', 'add', '-g', 'typeclaw@latest'] : ['pnpm', 'add', 'typeclaw@latest']
40
68
  case 'yarn':
41
- return ['yarn', 'global', 'upgrade', 'typeclaw', '--latest']
69
+ return scope === 'global'
70
+ ? ['yarn', 'global', 'upgrade', 'typeclaw', '--latest']
71
+ : ['yarn', 'upgrade', 'typeclaw', '--latest']
42
72
  }
43
73
  }
44
74
 
@@ -46,7 +76,18 @@ export function formatCommand(command: readonly string[]): string {
46
76
  return command.map(shellQuote).join(' ')
47
77
  }
48
78
 
49
- function detectInstallManager(packageJsonPath: string): UpdateManager | null {
79
+ function buildPlan(detected: DetectedInstall, packageJsonPath: string): SelfUpdatePlan {
80
+ return {
81
+ ok: true,
82
+ manager: detected.manager,
83
+ scope: detected.scope,
84
+ command: commandForInstall(detected.manager, detected.scope),
85
+ detectedFrom: packageJsonPath,
86
+ ...(detected.installRoot ? { cwd: detected.installRoot } : {}),
87
+ }
88
+ }
89
+
90
+ function detectInstall(packageJsonPath: string, fileExists: (path: string) => boolean): DetectedInstall | null {
50
91
  const parts = packageJsonPath.split(/[\\/]+/).filter(Boolean)
51
92
  const packageJson = parts[parts.length - 1]
52
93
  const packageName = parts[parts.length - 2]
@@ -61,23 +102,51 @@ function detectInstallManager(packageJsonPath: string): UpdateManager | null {
61
102
  parts[bunGlobalIdx + 2] === 'global' &&
62
103
  parts[bunGlobalIdx + 3] === 'node_modules'
63
104
  ) {
64
- return 'bun'
105
+ return { manager: 'bun', scope: 'global' }
65
106
  }
66
107
 
67
108
  // pnpm shards globals under a numeric major-version segment, e.g.
68
109
  // ~/Library/pnpm/global/5/node_modules or legacy ~/.pnpm-global/5/node_modules.
69
110
  if (nodeModulesIdx >= 2 && /^\d+$/.test(parts[nodeModulesIdx - 1] ?? '')) {
70
111
  const anchor = parts[nodeModulesIdx - 2]
71
- if (anchor === 'pnpm-global' || anchor === '.pnpm-global') return 'pnpm'
72
- if (anchor === 'global' && parts[nodeModulesIdx - 3] === 'pnpm') return 'pnpm'
112
+ if (anchor === 'pnpm-global' || anchor === '.pnpm-global') return { manager: 'pnpm', scope: 'global' }
113
+ if (anchor === 'global' && parts[nodeModulesIdx - 3] === 'pnpm') return { manager: 'pnpm', scope: 'global' }
73
114
  }
74
115
 
75
116
  if (nodeModulesIdx >= 2 && parts[nodeModulesIdx - 1] === 'global' && parts[nodeModulesIdx - 2] === 'yarn') {
76
- return 'yarn'
117
+ return { manager: 'yarn', scope: 'global' }
77
118
  }
78
119
 
79
- if (parts[nodeModulesIdx - 1] === 'lib') return 'npm'
80
- return null
120
+ if (parts[nodeModulesIdx - 1] === 'lib') return { manager: 'npm', scope: 'global' }
121
+
122
+ const installRoot = installRootFrom(packageJsonPath)
123
+ if (installRoot === null) return null
124
+ return { manager: detectLocalManager(installRoot, fileExists), scope: 'local', installRoot }
125
+ }
126
+
127
+ function installRootFrom(packageJsonPath: string): string | null {
128
+ const sepMatch = packageJsonPath.match(/[\\/]/)
129
+ const sep = sepMatch?.[0] ?? '/'
130
+ const parts = packageJsonPath.split(/[\\/]+/)
131
+ const nodeModulesIdx = parts.lastIndexOf('node_modules')
132
+ if (nodeModulesIdx <= 0) return null
133
+ const head = parts.slice(0, nodeModulesIdx)
134
+ // Preserve a leading separator on POSIX paths (`/usr/lib/...` splits to
135
+ // `['', 'usr', 'lib', ...]`) and Windows drive prefixes (`C:\Users\...`
136
+ // splits to `['C:', 'Users', ...]`, no leading empty).
137
+ return head.length === 1 && head[0] === '' ? sep : head.join(sep)
138
+ }
139
+
140
+ // Prefer the lockfile present in the install root. Probe order is bun -> pnpm
141
+ // -> yarn -> npm so a project with multiple lockfiles checked in (a real,
142
+ // gross-but-common scenario during migrations) lands on bun, matching this
143
+ // repo's default. The CLI's `--manager` flag always wins over this heuristic.
144
+ function detectLocalManager(installRoot: string, fileExists: (path: string) => boolean): UpdateManager {
145
+ if (fileExists(join(installRoot, 'bun.lock')) || fileExists(join(installRoot, 'bun.lockb'))) return 'bun'
146
+ if (fileExists(join(installRoot, 'pnpm-lock.yaml'))) return 'pnpm'
147
+ if (fileExists(join(installRoot, 'yarn.lock'))) return 'yarn'
148
+ if (fileExists(join(installRoot, 'package-lock.json'))) return 'npm'
149
+ return 'bun'
81
150
  }
82
151
 
83
152
  function shellQuote(arg: string): string {