typeclaw 0.26.0 → 0.28.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 (62) hide show
  1. package/package.json +1 -1
  2. package/scripts/generate-schema.ts +4 -6
  3. package/src/agent/index.ts +26 -4
  4. package/src/agent/multimodal/look-at.ts +1 -2
  5. package/src/agent/session-origin.ts +9 -1
  6. package/src/agent/tools/channel-fetch-attachment.ts +1 -2
  7. package/src/agent/tools/channel-react.ts +9 -3
  8. package/src/agent/tools/channel-reply.ts +30 -1
  9. package/src/agent/tools/channel-send.ts +94 -1
  10. package/src/bundled-plugins/github-cli-auth/gh-review-detect.ts +175 -0
  11. package/src/bundled-plugins/github-cli-auth/index.ts +4 -0
  12. package/src/bundled-plugins/github-cli-auth/review-recorder.ts +93 -0
  13. package/src/bundled-plugins/guard/policies/managed-config.ts +1 -1
  14. package/src/bundled-plugins/memory/README.md +3 -21
  15. package/src/bundled-plugins/memory/index.ts +1 -149
  16. package/src/bundled-plugins/reviewer/skills/code-review.ts +3 -1
  17. package/src/bundled-plugins/security/policies/cron-promotion.ts +2 -2
  18. package/src/channels/adapters/github/inbound.ts +155 -9
  19. package/src/channels/adapters/github/review-thread-resolver.ts +93 -8
  20. package/src/channels/github-false-receipt.ts +87 -0
  21. package/src/channels/github-review-claim.ts +91 -0
  22. package/src/channels/github-review-turn-ledger.ts +71 -0
  23. package/src/channels/persistence.ts +4 -102
  24. package/src/channels/router.ts +191 -7
  25. package/src/channels/schema.ts +20 -5
  26. package/src/cli/channel.ts +2 -1
  27. package/src/cli/init.ts +2 -1
  28. package/src/cli/inspect.ts +216 -36
  29. package/src/cli/logs.ts +15 -0
  30. package/src/cli/tui.ts +33 -39
  31. package/src/compose/logs.ts +1 -1
  32. package/src/config/config.ts +19 -288
  33. package/src/container/logs.ts +70 -22
  34. package/src/container/start.ts +0 -2
  35. package/src/cron/index.ts +3 -44
  36. package/src/cron/schema.ts +2 -96
  37. package/src/init/gitignore.ts +1 -2
  38. package/src/inspect/index.ts +128 -42
  39. package/src/inspect/item-list.ts +44 -0
  40. package/src/inspect/item.ts +17 -0
  41. package/src/inspect/label.ts +1 -1
  42. package/src/inspect/logs-item.ts +79 -0
  43. package/src/inspect/loop.ts +74 -3
  44. package/src/inspect/open-item.ts +100 -0
  45. package/src/inspect/preview.ts +106 -0
  46. package/src/inspect/session-list.ts +15 -3
  47. package/src/inspect/transcript-view.ts +182 -0
  48. package/src/inspect/tui-item.ts +97 -0
  49. package/src/secrets/defaults.ts +1 -18
  50. package/src/secrets/index.ts +0 -2
  51. package/src/secrets/schema.ts +4 -90
  52. package/src/secrets/storage.ts +0 -2
  53. package/src/server/index.ts +0 -4
  54. package/src/skills/typeclaw-channel-github/SKILL.md +3 -1
  55. package/src/skills/typeclaw-config/SKILL.md +9 -11
  56. package/src/skills/typeclaw-permissions/SKILL.md +1 -1
  57. package/src/tui/index.ts +72 -32
  58. package/typeclaw.schema.json +1 -0
  59. package/src/agent/tools/normalize-ref.ts +0 -11
  60. package/src/bundled-plugins/memory/migration.ts +0 -633
  61. package/src/secrets/migrate-kakaotalk.ts +0 -82
  62. package/src/secrets/migrate.ts +0 -96
@@ -1,82 +0,0 @@
1
- import { existsSync } from 'node:fs'
2
- import { rename } from 'node:fs/promises'
3
- import { join } from 'node:path'
4
-
5
- import { KakaoCredentialManager } from 'agent-messenger/kakaotalk'
6
- import type { KakaoConfig, PendingLoginState } from 'agent-messenger/kakaotalk'
7
-
8
- import { type KakaoChannelBlock, kakaoChannelBlockSchema } from './schema'
9
- import { SecretsBackend } from './storage'
10
-
11
- const KAKAO_CONFIG_DIR = join('workspace', '.agent-messenger')
12
- const CREDENTIALS_FILE = 'kakaotalk-credentials.json'
13
- const PENDING_LOGIN_FILE = 'kakaotalk-pending-login.json'
14
-
15
- export type KakaotalkCredentialMigrationResult = { promoted: boolean }
16
-
17
- export async function migrateKakaotalkCredentials(agentDir: string): Promise<KakaotalkCredentialMigrationResult> {
18
- const configDir = join(agentDir, KAKAO_CONFIG_DIR)
19
- const credentialsPath = join(configDir, CREDENTIALS_FILE)
20
- const pendingLoginPath = join(configDir, PENDING_LOGIN_FILE)
21
- if (!existsSync(credentialsPath) && !existsSync(pendingLoginPath)) return { promoted: false }
22
-
23
- const secretsPath = join(agentDir, 'secrets.json')
24
- const legacy = new KakaoCredentialManager(configDir)
25
- const config = await legacy.load()
26
- const pendingLogin = await legacy.loadPendingLogin()
27
- if (Object.keys(config.accounts).length === 0 && pendingLogin === null) return { promoted: false }
28
-
29
- const backend = new SecretsBackend(secretsPath)
30
- const result = await backend.updateChannelsAsync(async (channels) => {
31
- const existing = parseExistingBlock(channels.kakaotalk)
32
- const next = mergeLegacyBlock(existing, config, pendingLogin)
33
- if (next === existing) return { result: { promoted: false, renameCredentials: false, renamePending: false } }
34
-
35
- return {
36
- result: {
37
- promoted: true,
38
- renameCredentials: isEmptyBlock(existing) && Object.keys(config.accounts).length > 0,
39
- renamePending: pendingLogin !== null && existing?.pendingLogin === undefined,
40
- },
41
- next: { ...channels, kakaotalk: next },
42
- }
43
- })
44
- if (!result.promoted) return { promoted: false }
45
-
46
- if (result.renameCredentials) await renameIfPresent(credentialsPath, `${credentialsPath}.migrated`)
47
- if (result.renamePending) await renameIfPresent(pendingLoginPath, `${pendingLoginPath}.migrated`)
48
- return { promoted: true }
49
- }
50
-
51
- function parseExistingBlock(value: unknown): KakaoChannelBlock | null {
52
- if (value === undefined) return null
53
- return kakaoChannelBlockSchema.parse(value)
54
- }
55
-
56
- function isEmptyBlock(block: KakaoChannelBlock | null): boolean {
57
- return (
58
- block === null ||
59
- (block.currentAccount === null && Object.keys(block.accounts).length === 0 && block.pendingLogin === undefined)
60
- )
61
- }
62
-
63
- function mergeLegacyBlock(
64
- existing: KakaoChannelBlock | null,
65
- config: KakaoConfig,
66
- pendingLogin: PendingLoginState | null,
67
- ): KakaoChannelBlock | null {
68
- if (existing === null || isEmptyBlock(existing)) {
69
- return {
70
- currentAccount: config.current_account,
71
- accounts: config.accounts,
72
- ...(pendingLogin ? { pendingLogin } : {}),
73
- }
74
- }
75
- if (pendingLogin === null || existing.pendingLogin !== undefined) return existing
76
- return { ...existing, pendingLogin }
77
- }
78
-
79
- async function renameIfPresent(from: string, to: string): Promise<void> {
80
- if (!existsSync(from)) return
81
- await rename(from, to)
82
- }
@@ -1,96 +0,0 @@
1
- import { existsSync, readFileSync, renameSync, unlinkSync } from 'node:fs'
2
- import { join } from 'node:path'
3
-
4
- import { parseSecretsFile } from './schema'
5
-
6
- const LEGACY_FILENAME = 'auth.json'
7
- const TARGET_FILENAME = 'secrets.json'
8
-
9
- // One-shot rename of an old agent folder's auth.json to secrets.json. Called
10
- // from createSecretsStoreForAgent before the backend opens the file so the
11
- // rest of the storage pipeline only ever sees secrets.json. The rename runs
12
- // on every store construction because it's cheap (existsSync + early return
13
- // in the common case) and the rename itself is the state — no flag file.
14
- //
15
- // Cases:
16
- // 1. only auth.json exists -> renameSync to secrets.json
17
- // 2. only secrets.json -> no-op
18
- // 3. neither -> no-op (backend will seed secrets.json)
19
- // 4. both exist, auth.json is the empty seed envelope -> unlink auth.json
20
- // 5. both exist, secrets.json is the empty seed envelope -> renameSync auth.json over the empty seed
21
- // 6. both exist, both carry credentials -> throw, refuse to merge
22
- //
23
- // The "both non-empty" hard error matters: if a user copied an old agent
24
- // folder, edited auth.json by hand, AND a newer typeclaw later created
25
- // secrets.json with real credentials, we don't know which is the source of
26
- // truth. Loud failure beats silent merge.
27
- export function migrateLegacyAuthJson(agentDir: string): void {
28
- const legacyPath = join(agentDir, LEGACY_FILENAME)
29
- const targetPath = join(agentDir, TARGET_FILENAME)
30
-
31
- if (!existsSync(legacyPath)) return
32
-
33
- if (!existsSync(targetPath)) {
34
- renameWithRaceFallback(legacyPath, targetPath)
35
- return
36
- }
37
-
38
- if (isEmptyEnvelope(legacyPath)) {
39
- unlinkSync(legacyPath)
40
- return
41
- }
42
-
43
- if (isEmptyEnvelope(targetPath)) {
44
- // POSIX renameSync atomically replaces the destination; the empty
45
- // secrets.json is the safer thing to lose vs an auth.json with
46
- // credentials. Race-safe by the same reasoning as the no-target branch.
47
- renameWithRaceFallback(legacyPath, targetPath)
48
- return
49
- }
50
-
51
- throw new Error(
52
- `Both ${LEGACY_FILENAME} and a non-empty ${TARGET_FILENAME} exist in ${agentDir}. ` +
53
- `Inspect manually and remove the stale file before re-running.`,
54
- )
55
- }
56
-
57
- // renameSync is atomic per syscall, but two concurrent createSecretsStoreForAgent
58
- // callers can both observe `auth.json` exists and `secrets.json` does not, then
59
- // race on the rename. One wins; the other gets ENOENT because the legacy file
60
- // is already gone. That's effectively a successful migration from the loser's
61
- // POV — recheck the target and swallow the ENOENT.
62
- function renameWithRaceFallback(from: string, to: string): void {
63
- try {
64
- renameSync(from, to)
65
- } catch (err) {
66
- if ((err as NodeJS.ErrnoException).code === 'ENOENT' && existsSync(to)) {
67
- return
68
- }
69
- throw err
70
- }
71
- }
72
-
73
- // "Empty envelope" = no actual credentials. parseSecretsFile normalises both
74
- // legacy v1 and current v2 to a v2-shaped SecretsFile, so we only check the
75
- // v2 fields. We do NOT try to be clever about "approximately empty" — exact
76
- // emptiness is the only safe auto-delete / auto-overwrite case.
77
- function isEmptyEnvelope(path: string): boolean {
78
- let raw: string
79
- try {
80
- raw = readFileSync(path, 'utf8')
81
- } catch {
82
- return false
83
- }
84
- if (raw.trim() === '') return true
85
-
86
- let parsed: unknown
87
- try {
88
- parsed = JSON.parse(raw)
89
- } catch {
90
- return false
91
- }
92
-
93
- const result = parseSecretsFile(parsed)
94
- if (!result.ok) return false
95
- return Object.keys(result.file.providers).length === 0 && Object.keys(result.file.channels).length === 0
96
- }