typeclaw 0.37.4 → 0.37.6

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 (51) hide show
  1. package/package.json +1 -1
  2. package/src/agent/doctor.ts +6 -1
  3. package/src/agent/plugin-tools.ts +23 -1
  4. package/src/agent/subagents.ts +146 -14
  5. package/src/agent/todo/scope.ts +4 -2
  6. package/src/agent/tools/channel-reply.ts +7 -9
  7. package/src/bundled-plugins/doc-render/index.ts +10 -0
  8. package/src/bundled-plugins/doc-render/skills/typeclaw-render-pdf/SKILL.md +171 -165
  9. package/src/bundled-plugins/doc-render/templates/lib.typ +339 -0
  10. package/src/bundled-plugins/github-cli-auth/gh-command.ts +95 -11
  11. package/src/bundled-plugins/github-cli-auth/git-command.ts +11 -0
  12. package/src/bundled-plugins/github-cli-auth/index.ts +68 -7
  13. package/src/bundled-plugins/memory/index.ts +9 -6
  14. package/src/bundled-plugins/memory/load-memory.ts +16 -2
  15. package/src/bundled-plugins/memory/slug.ts +19 -0
  16. package/src/bundled-plugins/security/policies/private-surface-read.ts +4 -1
  17. package/src/channels/adapters/github/inbound.ts +68 -43
  18. package/src/channels/adapters/github/index.ts +57 -9
  19. package/src/channels/adapters/github/recover-failed-deliveries.ts +270 -0
  20. package/src/channels/adapters/kakaotalk.ts +5 -1
  21. package/src/channels/adapters/mention-hints.ts +17 -0
  22. package/src/channels/manager.ts +77 -1
  23. package/src/channels/router.ts +181 -12
  24. package/src/cli/compose.ts +11 -2
  25. package/src/cli/dreams.ts +2 -2
  26. package/src/cli/inspect.ts +2 -2
  27. package/src/cli/logs.ts +2 -2
  28. package/src/cli/mount.ts +5 -5
  29. package/src/cli/require-agent-dir.ts +31 -0
  30. package/src/cli/restart.ts +2 -1
  31. package/src/cli/shell.ts +2 -2
  32. package/src/cli/start.ts +2 -1
  33. package/src/cli/stop.ts +2 -2
  34. package/src/cli/tui.ts +20 -6
  35. package/src/cli/ui.ts +13 -0
  36. package/src/compose/restart.ts +1 -1
  37. package/src/compose/start.ts +4 -2
  38. package/src/config/config.ts +200 -9
  39. package/src/container/shared.ts +18 -0
  40. package/src/container/start.ts +1 -1
  41. package/src/cron/consumer.ts +3 -3
  42. package/src/hostd/client.ts +48 -52
  43. package/src/hostd/daemon.ts +82 -39
  44. package/src/hostd/paths.ts +22 -2
  45. package/src/hostd/spawn.ts +7 -0
  46. package/src/init/dockerfile.ts +11 -8
  47. package/src/init/kakaotalk-auth.ts +2 -2
  48. package/src/init/packagejson.ts +2 -2
  49. package/src/plugin/loader.ts +7 -4
  50. package/src/sandbox/session-tmp.ts +6 -1
  51. package/src/secrets/export-claude-credentials-file.ts +2 -2
@@ -1,6 +1,9 @@
1
+ import { createHash } from 'node:crypto'
1
2
  import { chmod, mkdir } from 'node:fs/promises'
2
- import { homedir } from 'node:os'
3
- import { join } from 'node:path'
3
+ import { homedir, userInfo } from 'node:os'
4
+ import { join, resolve } from 'node:path'
5
+
6
+ import { isWindows } from '@/shared'
4
7
 
5
8
  // Fixed in-container path where the host daemon's run dir is bind-mounted.
6
9
  // The agent uses this to reach the host daemon (e.g. for the `restart` tool).
@@ -33,9 +36,26 @@ export function logDir(): string {
33
36
  }
34
37
 
35
38
  export function socketPath(): string {
39
+ if (isWindows()) return windowsPipePath()
36
40
  return join(runDir(), SOCKET_FILE)
37
41
  }
38
42
 
43
+ function windowsPipePath(): string {
44
+ const uid =
45
+ typeof process.getuid === 'function'
46
+ ? `uid:${process.getuid()}`
47
+ : `user:${process.env.USERDOMAIN ?? ''}\\${userInfo().username}`
48
+ // Locale-invariant lowercasing: toLocaleLowerCase under e.g. tr-TR would map
49
+ // 'I' to a dotless 'ı', hashing the same path differently per process locale.
50
+ const scopedHome = resolve(homeRoot()).toLowerCase()
51
+ const hash = createHash('sha256').update(`${uid}\0${scopedHome}`).digest('hex').slice(0, 32)
52
+
53
+ // Node's net named-pipe API has no portable ACL hook. TypeClaw accepts that
54
+ // under the single-tenant dev-box model; the per-user/per-home pipe name keeps
55
+ // the pipe scoped, while the separate HTTP leg remains restart/secrets-only.
56
+ return `\\\\.\\pipe\\typeclaw-hostd-${hash}`
57
+ }
58
+
39
59
  export function pidfilePath(): string {
40
60
  return join(runDir(), 'hostd.pid')
41
61
  }
@@ -1,6 +1,8 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { open, readFile, unlink, writeFile } from 'node:fs/promises'
3
3
 
4
+ import { isWindows } from '@/shared'
5
+
4
6
  import { isDaemonReachable, send } from './client'
5
7
  import { ensureDirs, lockfilePath, logfilePath, pidfilePath, socketPath } from './paths'
6
8
  import type { HttpInfoResult, VersionResult } from './protocol'
@@ -75,6 +77,11 @@ async function requestShutdownAndWait(): Promise<boolean> {
75
77
  if (!reply.ok) return false
76
78
  const deadline = Date.now() + SHUTDOWN_TIMEOUT_MS
77
79
  while (Date.now() < deadline) {
80
+ if (isWindows()) {
81
+ if (!(await isDaemonReachable(POLL_INTERVAL_MS))) return true
82
+ await sleep(POLL_INTERVAL_MS)
83
+ continue
84
+ }
78
85
  if (!existsSync(socketPath())) return true
79
86
  await sleep(POLL_INTERVAL_MS)
80
87
  }
@@ -358,9 +358,9 @@ set -eu
358
358
  # The persist root lives under /agent/.typeclaw/home/ (bind-mounted
359
359
  # from the agent folder via the -v <cwd>:/agent flag in start.ts).
360
360
  # Namespacing under .typeclaw/ keeps the agent's top-level layout clean and reserves
361
- # a system-owned subtree we can extend later (e.g. ~/.gemini/,
362
- # ~/.config/<tool>/) without colliding with user files. The directory
363
- # is gitignored by buildGitignore() so credentials never enter history.
361
+ # a system-owned subtree we can extend later (e.g. ~/.gemini/) without
362
+ # colliding with user files. The directory is gitignored by buildGitignore()
363
+ # so credentials never enter history.
364
364
  #
365
365
  # Three invariants this function enforces:
366
366
  #
@@ -372,11 +372,11 @@ set -eu
372
372
  # if a previous container life happened to write a real ~/.codex/
373
373
  # dir before this code shipped.
374
374
  #
375
- # 2. We symlink the FILE, not the directory. Codex writes other state
376
- # to ~/.codex/ over time (history.jsonl, log/, config.toml). Linking
377
- # only auth.json keeps the persistence scope tight to credentials;
378
- # history/logs stay ephemeral by design. Future credentials get
379
- # added file-by-file here, not by widening to a directory link.
375
+ # 2. We symlink credential FILES for tools whose config dirs are mostly
376
+ # scratch/history (Codex, Claude). We do not redirect global config
377
+ # locations such as XDG_CONFIG_HOME or ~/.config here because tools like
378
+ # git also read config from those paths; first-party bundles that need
379
+ # persistence should set their own app-specific env vars instead.
380
380
  #
381
381
  # 3. We mkdir -p the target's parent on every boot. /agent is bind-
382
382
  # mounted, so the host-side path may exist or not depending on
@@ -1264,6 +1264,9 @@ ${fromAndHeavyLayers}
1264
1264
 
1265
1265
  ENV NODE_ENV=production
1266
1266
 
1267
+ # Persist first-party GWS config without changing global XDG/git config lookup.
1268
+ ENV GWS_CONFIG_HOME=/agent/workspace/.config/gws
1269
+
1267
1270
  # Keep agent-messenger's fallback config dir inside workspace/ for any future
1268
1271
  # SDK fallback paths. TypeClaw's KakaoTalk adapter does not write there:
1269
1272
  # credentials live in secrets.json#channels.kakaotalk and container writes go
@@ -1,4 +1,4 @@
1
- import { join, resolve } from 'node:path'
1
+ import { join, posix, resolve } from 'node:path'
2
2
 
3
3
  import { loginFlow as upstreamLoginFlow } from 'agent-messenger/kakaotalk'
4
4
 
@@ -34,7 +34,7 @@ export type LoginFlowOptions = Parameters<LoginFlowFn>[0]
34
34
  export type LoginFlowResult = Awaited<ReturnType<LoginFlowFn>>
35
35
 
36
36
  export function kakaotalkConfigDir(agentDir: string): string {
37
- return join(agentDir, 'workspace', '.agent-messenger')
37
+ return posix.join(agentDir, 'workspace', '.agent-messenger')
38
38
  }
39
39
 
40
40
  export function kakaotalkSecretsPath(agentDir: string): string {
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { mkdir, readFile, writeFile } from 'node:fs/promises'
3
- import { join } from 'node:path'
3
+ import { join, posix } from 'node:path'
4
4
 
5
5
  import { GITKEEP_FILE, PACKAGES_DIR } from './paths'
6
6
 
@@ -33,7 +33,7 @@ export async function refreshPackageJson(cwd: string): Promise<PackageJsonRefres
33
33
  if (updated) changed.push(PACKAGE_FILE)
34
34
  }
35
35
 
36
- const gitkeepRel = join(PACKAGES_DIR, GITKEEP_FILE)
36
+ const gitkeepRel = posix.join(PACKAGES_DIR, GITKEEP_FILE)
37
37
  const gitkeepPath = join(cwd, gitkeepRel)
38
38
  if (!existsSync(gitkeepPath)) {
39
39
  await mkdir(join(cwd, PACKAGES_DIR), { recursive: true })
@@ -61,8 +61,7 @@ async function loadLocal(entry: string, agentDir: string): Promise<ResolvedPlugi
61
61
  if (!existsSync(resolved)) {
62
62
  throw new PluginNotFoundError(entry, `plugin path does not exist: ${entry} (resolved to ${resolved})`)
63
63
  }
64
- const url = pathToFileURL(resolved).href
65
- const mod = (await import(url)) as { default?: unknown }
64
+ const mod = (await import(toModuleSpecifier(resolved))) as { default?: unknown }
66
65
  const defined = expectDefined(mod, entry)
67
66
  const name = basename(resolved).replace(/\.(ts|tsx|js|mjs|cjs)$/i, '')
68
67
  return { name, version: undefined, source: entry, defined }
@@ -108,10 +107,10 @@ async function loadNpm(entry: string, agentDir: string): Promise<ResolvedPlugin>
108
107
  // located on disk; the else branch lets Bun's resolver read `exports` maps.
109
108
  let importTarget: string
110
109
  if (entryPath !== null) {
111
- importTarget = pathToFileURL(entryPath).href
110
+ importTarget = toModuleSpecifier(entryPath)
112
111
  } else {
113
112
  try {
114
- importTarget = Bun.resolveSync(packageName, agentDir)
113
+ importTarget = toModuleSpecifier(Bun.resolveSync(packageName, agentDir))
115
114
  } catch (err) {
116
115
  throw new PluginNotFoundError(entry, `cannot resolve plugin "${entry}": ${describeError(err)}`, { cause: err })
117
116
  }
@@ -151,6 +150,10 @@ function describeError(err: unknown): string {
151
150
  return err instanceof Error ? err.message : String(err)
152
151
  }
153
152
 
153
+ function toModuleSpecifier(target: string): string {
154
+ return isAbsolute(target) ? pathToFileURL(target).href : target
155
+ }
156
+
154
157
  function findPackageJson(entry: string, agentDir: string): string | null {
155
158
  const PACKAGE_JSON = 'package.json'
156
159
  let cur = agentDir
@@ -1,5 +1,10 @@
1
1
  import { mkdir } from 'node:fs/promises'
2
- import { isAbsolute, join, relative, resolve } from 'node:path'
2
+ import { posix } from 'node:path'
3
+
4
+ // Container-only code over the POSIX `/tmp`; pinned to `path.posix` so the test
5
+ // suite produces the same backing paths on a win32 runner (default `node:path`
6
+ // would yield `\tmp\…` and diverge from the Linux runtime).
7
+ const { isAbsolute, join, relative, resolve } = posix
3
8
 
4
9
  // Per-session scratch lives on the REAL container /tmp, namespaced by session id.
5
10
  // It sits OUTSIDE the agent folder on purpose: the agent folder's `sessions/` is
@@ -9,7 +9,7 @@ import {
9
9
  writeFileSync,
10
10
  } from 'node:fs'
11
11
  import { homedir } from 'node:os'
12
- import { dirname, isAbsolute, join, resolve } from 'node:path'
12
+ import { dirname, isAbsolute, join, posix, resolve } from 'node:path'
13
13
 
14
14
  import { decodeClaudeAccessTokenExpiryMs, emitClaudeCredentialsJson } from './claude-credentials-json'
15
15
  import type { ProviderCredential, Providers } from './schema'
@@ -19,7 +19,7 @@ const FILE_MODE = 0o600
19
19
  const DIR_MODE = 0o700
20
20
  export const CLAUDE_CREDENTIALS_FILE_NAME = '.credentials.json'
21
21
  export const CLAUDE_DEFAULT_CONFIG_DIR_NAME = '.claude'
22
- export const CLAUDE_CREDENTIALS_RELATIVE_PATH = join(CLAUDE_DEFAULT_CONFIG_DIR_NAME, CLAUDE_CREDENTIALS_FILE_NAME)
22
+ export const CLAUDE_CREDENTIALS_RELATIVE_PATH = posix.join(CLAUDE_DEFAULT_CONFIG_DIR_NAME, CLAUDE_CREDENTIALS_FILE_NAME)
23
23
 
24
24
  export type ExportClaudeCredentialsFileResult =
25
25
  | { action: 'skipped'; reason: SkipReason }