typeclaw 0.27.0 → 0.28.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/package.json +1 -1
- package/scripts/generate-schema.ts +4 -6
- package/src/agent/index.ts +26 -4
- package/src/agent/multimodal/look-at.ts +1 -2
- package/src/agent/provider-error.ts +33 -1
- package/src/agent/tools/channel-fetch-attachment.ts +1 -2
- package/src/agent/tools/channel-react.ts +9 -3
- package/src/agent/tools/channel-reply.ts +52 -1
- package/src/agent/tools/channel-send.ts +115 -1
- package/src/bundled-plugins/github-cli-auth/gh-review-detect.ts +175 -0
- package/src/bundled-plugins/github-cli-auth/index.ts +4 -0
- package/src/bundled-plugins/github-cli-auth/review-recorder.ts +93 -0
- package/src/bundled-plugins/guard/policies/managed-config.ts +1 -1
- package/src/bundled-plugins/memory/README.md +3 -21
- package/src/bundled-plugins/memory/index.ts +1 -149
- package/src/bundled-plugins/security/policies/cron-promotion.ts +2 -2
- package/src/channels/adapters/github/inbound.ts +103 -0
- package/src/channels/adapters/github/index.ts +10 -0
- package/src/channels/adapters/github/review-state.ts +137 -0
- package/src/channels/adapters/github/review-thread-resolver.ts +65 -5
- package/src/channels/github-false-receipt.ts +87 -0
- package/src/channels/github-rereview-guard.ts +76 -0
- package/src/channels/github-review-claim.ts +92 -0
- package/src/channels/github-review-turn-ledger.ts +71 -0
- package/src/channels/persistence.ts +4 -102
- package/src/channels/router.ts +181 -7
- package/src/channels/schema.ts +20 -5
- package/src/channels/types.ts +31 -0
- package/src/cli/channel.ts +2 -1
- package/src/cli/init.ts +2 -1
- package/src/config/config.ts +19 -288
- package/src/container/start.ts +0 -2
- package/src/cron/index.ts +3 -44
- package/src/cron/schema.ts +2 -96
- package/src/init/gitignore.ts +1 -2
- package/src/inspect/transcript-view.ts +10 -0
- package/src/secrets/defaults.ts +1 -18
- package/src/secrets/index.ts +0 -2
- package/src/secrets/schema.ts +4 -90
- package/src/secrets/storage.ts +0 -2
- package/src/server/index.ts +11 -5
- package/src/shared/protocol.ts +18 -6
- package/src/skills/typeclaw-config/SKILL.md +9 -11
- package/src/skills/typeclaw-permissions/SKILL.md +1 -1
- package/src/tui/format.ts +13 -0
- package/src/tui/index.ts +21 -7
- package/typeclaw.schema.json +1 -0
- package/src/agent/tools/normalize-ref.ts +0 -11
- package/src/bundled-plugins/memory/migration.ts +0 -633
- package/src/secrets/migrate-kakaotalk.ts +0 -82
- 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
|
-
}
|
package/src/secrets/migrate.ts
DELETED
|
@@ -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
|
-
}
|