typeclaw 0.1.2 → 0.1.3

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 (42) hide show
  1. package/README.md +4 -0
  2. package/auth.schema.json +238 -7
  3. package/package.json +1 -1
  4. package/secrets.schema.json +238 -7
  5. package/src/agent/auth.ts +19 -38
  6. package/src/agent/tools/channel-fetch-attachment.ts +6 -0
  7. package/src/agent/tools/channel-history.ts +10 -1
  8. package/src/agent/tools/channel-log.ts +32 -0
  9. package/src/agent/tools/channel-reply.ts +18 -1
  10. package/src/agent/tools/channel-send.ts +13 -1
  11. package/src/bundled-plugins/tool-result-cap/README.md +67 -0
  12. package/src/bundled-plugins/tool-result-cap/cap-result.ts +56 -0
  13. package/src/bundled-plugins/tool-result-cap/index.ts +51 -0
  14. package/src/channels/adapters/kakaotalk.ts +25 -16
  15. package/src/channels/manager.ts +47 -38
  16. package/src/cli/channel.ts +3 -3
  17. package/src/cli/index.ts +3 -0
  18. package/src/cli/init.ts +2 -1
  19. package/src/cli/ui.ts +11 -0
  20. package/src/config/config.ts +15 -4
  21. package/src/container/start.ts +90 -1
  22. package/src/hostd/daemon.ts +28 -3
  23. package/src/hostd/protocol.ts +7 -0
  24. package/src/init/auto-upgrade.ts +368 -0
  25. package/src/init/dockerfile.ts +25 -14
  26. package/src/init/index.ts +123 -77
  27. package/src/init/kakaotalk-auth.ts +9 -3
  28. package/src/init/run-bun-install.ts +34 -0
  29. package/src/run/bundled-plugins.ts +7 -0
  30. package/src/run/index.ts +9 -0
  31. package/src/secrets/defaults.ts +67 -0
  32. package/src/secrets/hydrate.ts +99 -0
  33. package/src/secrets/index.ts +6 -12
  34. package/src/secrets/kakao-store.ts +129 -0
  35. package/src/secrets/migrate-kakaotalk.ts +82 -0
  36. package/src/secrets/migrate.ts +5 -4
  37. package/src/secrets/resolve.ts +57 -0
  38. package/src/secrets/schema.ts +162 -42
  39. package/src/secrets/storage.ts +253 -47
  40. package/src/skills/typeclaw-config/SKILL.md +47 -8
  41. package/typeclaw.schema.json +36 -2
  42. package/src/secrets/env.ts +0 -43
package/README.md CHANGED
@@ -110,6 +110,10 @@ my-agent/
110
110
 
111
111
  `Dockerfile` and `.gitignore` are owned by TypeClaw and rewritten on every `start` — edit `src/init/dockerfile.ts` and re-run `start --build` to ship template changes.
112
112
 
113
+ ### Secrets
114
+
115
+ Credentials live in two gitignored files: `.env` (plain `KEY=value` lines, injected into the container via `--env-file`) and `secrets.json` (a structured store managed by TypeClaw). **Env-wins**: when a credential's canonical env var (e.g. `FIREWORKS_API_KEY`, `SLACK_BOT_TOKEN`) is set, that value is used at runtime — `secrets.json` is never auto-mutated to capture it. Every secret-bearing field in `secrets.json` is a `Secret` (`string | { value?, env? }`), so the file can rebind a credential to a custom env-var name on demand. See [AGENTS.md § Secrets](./AGENTS.md#secrets) for the full contract.
116
+
113
117
  ## Development
114
118
 
115
119
  ```sh
package/auth.schema.json CHANGED
@@ -7,10 +7,9 @@
7
7
  },
8
8
  "version": {
9
9
  "type": "number",
10
- "const": 1
10
+ "const": 2
11
11
  },
12
- "llm": {
13
- "default": {},
12
+ "providers": {
14
13
  "type": "object",
15
14
  "propertyNames": {
16
15
  "type": "string"
@@ -25,8 +24,25 @@
25
24
  "const": "api_key"
26
25
  },
27
26
  "key": {
28
- "type": "string",
29
- "minLength": 1
27
+ "anyOf": [
28
+ {
29
+ "type": "string",
30
+ "minLength": 1
31
+ },
32
+ {
33
+ "type": "object",
34
+ "properties": {
35
+ "value": {
36
+ "type": "string",
37
+ "minLength": 1
38
+ },
39
+ "env": {
40
+ "type": "string",
41
+ "minLength": 1
42
+ }
43
+ }
44
+ }
45
+ ]
30
46
  }
31
47
  },
32
48
  "required": [
@@ -51,9 +67,224 @@
51
67
  }
52
68
  },
53
69
  "channels": {
54
- "default": {},
55
70
  "type": "object",
56
- "properties": {},
71
+ "properties": {
72
+ "slack-bot": {
73
+ "type": "object",
74
+ "properties": {
75
+ "botToken": {
76
+ "anyOf": [
77
+ {
78
+ "type": "string",
79
+ "minLength": 1
80
+ },
81
+ {
82
+ "type": "object",
83
+ "properties": {
84
+ "value": {
85
+ "type": "string",
86
+ "minLength": 1
87
+ },
88
+ "env": {
89
+ "type": "string",
90
+ "minLength": 1
91
+ }
92
+ }
93
+ }
94
+ ]
95
+ },
96
+ "appToken": {
97
+ "anyOf": [
98
+ {
99
+ "type": "string",
100
+ "minLength": 1
101
+ },
102
+ {
103
+ "type": "object",
104
+ "properties": {
105
+ "value": {
106
+ "type": "string",
107
+ "minLength": 1
108
+ },
109
+ "env": {
110
+ "type": "string",
111
+ "minLength": 1
112
+ }
113
+ }
114
+ }
115
+ ]
116
+ }
117
+ }
118
+ },
119
+ "discord-bot": {
120
+ "type": "object",
121
+ "properties": {
122
+ "token": {
123
+ "anyOf": [
124
+ {
125
+ "type": "string",
126
+ "minLength": 1
127
+ },
128
+ {
129
+ "type": "object",
130
+ "properties": {
131
+ "value": {
132
+ "type": "string",
133
+ "minLength": 1
134
+ },
135
+ "env": {
136
+ "type": "string",
137
+ "minLength": 1
138
+ }
139
+ }
140
+ }
141
+ ]
142
+ }
143
+ }
144
+ },
145
+ "telegram-bot": {
146
+ "type": "object",
147
+ "properties": {
148
+ "token": {
149
+ "anyOf": [
150
+ {
151
+ "type": "string",
152
+ "minLength": 1
153
+ },
154
+ {
155
+ "type": "object",
156
+ "properties": {
157
+ "value": {
158
+ "type": "string",
159
+ "minLength": 1
160
+ },
161
+ "env": {
162
+ "type": "string",
163
+ "minLength": 1
164
+ }
165
+ }
166
+ }
167
+ ]
168
+ }
169
+ }
170
+ },
171
+ "kakaotalk": {
172
+ "type": "object",
173
+ "properties": {
174
+ "currentAccount": {
175
+ "anyOf": [
176
+ {
177
+ "type": "string"
178
+ },
179
+ {
180
+ "type": "null"
181
+ }
182
+ ]
183
+ },
184
+ "accounts": {
185
+ "type": "object",
186
+ "propertyNames": {
187
+ "type": "string"
188
+ },
189
+ "additionalProperties": {
190
+ "type": "object",
191
+ "properties": {
192
+ "account_id": {
193
+ "type": "string"
194
+ },
195
+ "oauth_token": {
196
+ "type": "string"
197
+ },
198
+ "user_id": {
199
+ "type": "string"
200
+ },
201
+ "refresh_token": {
202
+ "type": "string"
203
+ },
204
+ "device_uuid": {
205
+ "type": "string"
206
+ },
207
+ "device_type": {
208
+ "anyOf": [
209
+ {
210
+ "type": "string",
211
+ "const": "pc"
212
+ },
213
+ {
214
+ "type": "string",
215
+ "const": "tablet"
216
+ }
217
+ ]
218
+ },
219
+ "auth_method": {
220
+ "anyOf": [
221
+ {
222
+ "type": "string",
223
+ "const": "login"
224
+ },
225
+ {
226
+ "type": "string",
227
+ "const": "extract"
228
+ }
229
+ ]
230
+ },
231
+ "created_at": {
232
+ "type": "string"
233
+ },
234
+ "updated_at": {
235
+ "type": "string"
236
+ }
237
+ },
238
+ "required": [
239
+ "account_id",
240
+ "oauth_token",
241
+ "user_id",
242
+ "device_uuid",
243
+ "device_type",
244
+ "created_at",
245
+ "updated_at"
246
+ ]
247
+ }
248
+ },
249
+ "pendingLogin": {
250
+ "type": "object",
251
+ "properties": {
252
+ "device_uuid": {
253
+ "type": "string"
254
+ },
255
+ "device_type": {
256
+ "anyOf": [
257
+ {
258
+ "type": "string",
259
+ "const": "pc"
260
+ },
261
+ {
262
+ "type": "string",
263
+ "const": "tablet"
264
+ }
265
+ ]
266
+ },
267
+ "email": {
268
+ "type": "string"
269
+ },
270
+ "created_at": {
271
+ "type": "string"
272
+ }
273
+ },
274
+ "required": [
275
+ "device_uuid",
276
+ "device_type",
277
+ "email",
278
+ "created_at"
279
+ ]
280
+ }
281
+ },
282
+ "required": [
283
+ "currentAccount",
284
+ "accounts"
285
+ ]
286
+ }
287
+ },
57
288
  "additionalProperties": {}
58
289
  }
59
290
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typeclaw",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "homepage": "https://github.com/typeclaw/typeclaw#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/typeclaw/typeclaw/issues"
@@ -7,10 +7,9 @@
7
7
  },
8
8
  "version": {
9
9
  "type": "number",
10
- "const": 1
10
+ "const": 2
11
11
  },
12
- "llm": {
13
- "default": {},
12
+ "providers": {
14
13
  "type": "object",
15
14
  "propertyNames": {
16
15
  "type": "string"
@@ -25,8 +24,25 @@
25
24
  "const": "api_key"
26
25
  },
27
26
  "key": {
28
- "type": "string",
29
- "minLength": 1
27
+ "anyOf": [
28
+ {
29
+ "type": "string",
30
+ "minLength": 1
31
+ },
32
+ {
33
+ "type": "object",
34
+ "properties": {
35
+ "value": {
36
+ "type": "string",
37
+ "minLength": 1
38
+ },
39
+ "env": {
40
+ "type": "string",
41
+ "minLength": 1
42
+ }
43
+ }
44
+ }
45
+ ]
30
46
  }
31
47
  },
32
48
  "required": [
@@ -51,9 +67,224 @@
51
67
  }
52
68
  },
53
69
  "channels": {
54
- "default": {},
55
70
  "type": "object",
56
- "properties": {},
71
+ "properties": {
72
+ "slack-bot": {
73
+ "type": "object",
74
+ "properties": {
75
+ "botToken": {
76
+ "anyOf": [
77
+ {
78
+ "type": "string",
79
+ "minLength": 1
80
+ },
81
+ {
82
+ "type": "object",
83
+ "properties": {
84
+ "value": {
85
+ "type": "string",
86
+ "minLength": 1
87
+ },
88
+ "env": {
89
+ "type": "string",
90
+ "minLength": 1
91
+ }
92
+ }
93
+ }
94
+ ]
95
+ },
96
+ "appToken": {
97
+ "anyOf": [
98
+ {
99
+ "type": "string",
100
+ "minLength": 1
101
+ },
102
+ {
103
+ "type": "object",
104
+ "properties": {
105
+ "value": {
106
+ "type": "string",
107
+ "minLength": 1
108
+ },
109
+ "env": {
110
+ "type": "string",
111
+ "minLength": 1
112
+ }
113
+ }
114
+ }
115
+ ]
116
+ }
117
+ }
118
+ },
119
+ "discord-bot": {
120
+ "type": "object",
121
+ "properties": {
122
+ "token": {
123
+ "anyOf": [
124
+ {
125
+ "type": "string",
126
+ "minLength": 1
127
+ },
128
+ {
129
+ "type": "object",
130
+ "properties": {
131
+ "value": {
132
+ "type": "string",
133
+ "minLength": 1
134
+ },
135
+ "env": {
136
+ "type": "string",
137
+ "minLength": 1
138
+ }
139
+ }
140
+ }
141
+ ]
142
+ }
143
+ }
144
+ },
145
+ "telegram-bot": {
146
+ "type": "object",
147
+ "properties": {
148
+ "token": {
149
+ "anyOf": [
150
+ {
151
+ "type": "string",
152
+ "minLength": 1
153
+ },
154
+ {
155
+ "type": "object",
156
+ "properties": {
157
+ "value": {
158
+ "type": "string",
159
+ "minLength": 1
160
+ },
161
+ "env": {
162
+ "type": "string",
163
+ "minLength": 1
164
+ }
165
+ }
166
+ }
167
+ ]
168
+ }
169
+ }
170
+ },
171
+ "kakaotalk": {
172
+ "type": "object",
173
+ "properties": {
174
+ "currentAccount": {
175
+ "anyOf": [
176
+ {
177
+ "type": "string"
178
+ },
179
+ {
180
+ "type": "null"
181
+ }
182
+ ]
183
+ },
184
+ "accounts": {
185
+ "type": "object",
186
+ "propertyNames": {
187
+ "type": "string"
188
+ },
189
+ "additionalProperties": {
190
+ "type": "object",
191
+ "properties": {
192
+ "account_id": {
193
+ "type": "string"
194
+ },
195
+ "oauth_token": {
196
+ "type": "string"
197
+ },
198
+ "user_id": {
199
+ "type": "string"
200
+ },
201
+ "refresh_token": {
202
+ "type": "string"
203
+ },
204
+ "device_uuid": {
205
+ "type": "string"
206
+ },
207
+ "device_type": {
208
+ "anyOf": [
209
+ {
210
+ "type": "string",
211
+ "const": "pc"
212
+ },
213
+ {
214
+ "type": "string",
215
+ "const": "tablet"
216
+ }
217
+ ]
218
+ },
219
+ "auth_method": {
220
+ "anyOf": [
221
+ {
222
+ "type": "string",
223
+ "const": "login"
224
+ },
225
+ {
226
+ "type": "string",
227
+ "const": "extract"
228
+ }
229
+ ]
230
+ },
231
+ "created_at": {
232
+ "type": "string"
233
+ },
234
+ "updated_at": {
235
+ "type": "string"
236
+ }
237
+ },
238
+ "required": [
239
+ "account_id",
240
+ "oauth_token",
241
+ "user_id",
242
+ "device_uuid",
243
+ "device_type",
244
+ "created_at",
245
+ "updated_at"
246
+ ]
247
+ }
248
+ },
249
+ "pendingLogin": {
250
+ "type": "object",
251
+ "properties": {
252
+ "device_uuid": {
253
+ "type": "string"
254
+ },
255
+ "device_type": {
256
+ "anyOf": [
257
+ {
258
+ "type": "string",
259
+ "const": "pc"
260
+ },
261
+ {
262
+ "type": "string",
263
+ "const": "tablet"
264
+ }
265
+ ]
266
+ },
267
+ "email": {
268
+ "type": "string"
269
+ },
270
+ "created_at": {
271
+ "type": "string"
272
+ }
273
+ },
274
+ "required": [
275
+ "device_uuid",
276
+ "device_type",
277
+ "email",
278
+ "created_at"
279
+ ]
280
+ }
281
+ },
282
+ "required": [
283
+ "currentAccount",
284
+ "accounts"
285
+ ]
286
+ }
287
+ },
57
288
  "additionalProperties": {}
58
289
  }
59
290
  },
package/src/agent/auth.ts CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  supportsOAuth,
11
11
  type KnownProviderId,
12
12
  } from '@/config/providers'
13
- import { createSecretsStoreForAgent, stripEnvKey } from '@/secrets'
13
+ import { createSecretsStoreForAgent } from '@/secrets'
14
14
 
15
15
  type Auth = {
16
16
  authStorage: AuthStorage
@@ -19,10 +19,6 @@ type Auth = {
19
19
 
20
20
  const TEST_DUMMY_API_KEY = 'test_dummy_key'
21
21
 
22
- // In container stage, /agent is the bind-mounted agent folder; in host stage
23
- // (only used by `typeclaw init` itself), it falls back to process.cwd(). The
24
- // host writes secrets.json at init time and the container reads + refreshes
25
- // it at runtime — both paths point at the same file on the host filesystem.
26
22
  function secretsJsonPath(): string {
27
23
  return join(process.cwd(), 'secrets.json')
28
24
  }
@@ -35,10 +31,6 @@ export function getAuth(): Auth {
35
31
  const providerId = providerForModelRef(getConfig().model)
36
32
  const provider = KNOWN_PROVIDERS[providerId]
37
33
 
38
- // Bun sets NODE_ENV=test automatically under `bun test`. The dummy path
39
- // bypasses both secrets.json and process.env so suites that build sessions
40
- // but never hit the LLM don't need real credentials; production still
41
- // hard-exits to surface misconfiguration.
42
34
  if (process.env.NODE_ENV === 'test' && !hasAnyCredentialInEnv(provider.apiKeyEnv)) {
43
35
  const authStorage = AuthStorage.inMemory()
44
36
  if (supportsApiKey(provider)) {
@@ -51,42 +43,31 @@ export function getAuth(): Auth {
51
43
 
52
44
  const authStorage = createSecretsStoreForAgent(secretsJsonPath())
53
45
 
54
- // Persist the .env API key into secrets.json so the file is the single
55
- // source of truth for credentials. Upstream pi-ai's `getEnvApiKey()` only
56
- // knows about a hardcoded set of providers (anthropic, openai, etc.) and
57
- // does NOT know about Fireworks, so `hasAuth("fireworks")` returns false
58
- // unless a credential is materialized into AuthStorage's data map. Before
59
- // this migration the code used `setRuntimeApiKey`, which papered over the
60
- // gap in-memory but never wrote secrets.json — leaving `llm` empty for every
61
- // downstream consumer (rotation, audit, transport over the daemon
62
- // boundary) that treats the file as authoritative.
46
+ // Env-wins for api-key providers: when the canonical env var is set, layer
47
+ // that value in via setRuntimeApiKey so AuthStorage's hasAuth resolves
48
+ // true without persisting anything to secrets.json. This is the explicit
49
+ // reversal of the pre-v2 auto-migrate-to-file behaviour.
63
50
  //
64
- // Policy: never overwrite an existing OAuth credential. The user
65
- // explicitly logged in at init, and an unrelated `.env` value must not
66
- // silently displace it. Only write when no credential exists, or when an
67
- // existing api-key value drifted from the env var (the user rotated the
68
- // key in .env and expects the next boot to pick it up).
51
+ // setRuntimeApiKey is in-memory only (it writes to runtimeOverrides, never
52
+ // through withLock), so the file remains untouched even when the env var
53
+ // is set. OAuth credentials in the file still take precedence on read
54
+ // because AuthStorage's hasAuth checks runtimeOverrides first only for
55
+ // api-key-shaped credentials OAuth on disk wins on its own.
56
+ //
57
+ // The previous code branch that wrote the env value into secrets.json and
58
+ // stripped the matching `.env` line has been removed. Env values stay in
59
+ // env; the file stays user-owned. See src/secrets/hydrate.ts for the same
60
+ // policy on the channels side.
69
61
  if (supportsApiKey(provider) && provider.apiKeyEnv) {
70
62
  const envKey = process.env[provider.apiKeyEnv]
71
- if (envKey) {
63
+ if (envKey !== undefined && envKey !== '') {
72
64
  const existing = authStorage.get(provider.id)
73
- const apiKeyOwned = existing === undefined || existing.type === 'api_key'
74
- if (apiKeyOwned) {
75
- if (existing === undefined || existing.key !== envKey) {
76
- authStorage.set(provider.id, { type: 'api_key', key: envKey })
77
- }
78
- // secrets.json is now authoritative for this provider's api-key credential.
79
- // Strip the value from `.env` so the next boot does not silently revive a
80
- // stale or rotated-away key, and so users have a single place to edit.
81
- stripEnvKey(join(process.cwd(), '.env'), provider.apiKeyEnv)
65
+ if (existing === undefined || existing.type === 'api_key') {
66
+ authStorage.setRuntimeApiKey(provider.id, envKey)
82
67
  }
83
68
  }
84
69
  }
85
70
 
86
- // OAuth providers persist via `oauth-login.ts` at init time; api-key
87
- // providers persist via the migration block above. By this point
88
- // secrets.json is authoritative — a missing entry means the user skipped
89
- // login at init, deleted the file, or never set the provider's env var.
90
71
  if (!authStorage.hasAuth(provider.id)) {
91
72
  console.error(missingCredentialMessage(providerId))
92
73
  process.exit(1)
@@ -119,7 +100,7 @@ function missingCredentialMessage(providerId: KnownProviderId): string {
119
100
  return `No credentials for ${provider.name}. Run \`typeclaw init\` and pick "OAuth" to log in to ${modelName}.`
120
101
  }
121
102
  if (apiKeyOnly && provider.apiKeyEnv) {
122
- return `Set ${provider.apiKeyEnv} in .env to use ${modelName} via ${provider.name}.`
103
+ return `Set ${provider.apiKeyEnv} in .env (or secrets.json#providers.${provider.id}.key.value) to use ${modelName} via ${provider.name}.`
123
104
  }
124
105
  return `No credentials for ${provider.name}. Either set ${provider.apiKeyEnv ?? '<api-key-env>'} in .env or run \`typeclaw init\` and pick "OAuth".`
125
106
  }