@reclaimprotocol/agent 0.1.0-dev.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.
Files changed (191) hide show
  1. package/README.md +287 -0
  2. package/lib/cdp/capture.d.ts +22 -0
  3. package/lib/cdp/capture.js +159 -0
  4. package/lib/cdp/client.d.ts +9 -0
  5. package/lib/cdp/client.js +11 -0
  6. package/lib/cdp/eviction.d.ts +16 -0
  7. package/lib/cdp/eviction.js +38 -0
  8. package/lib/cdp/launcher.d.ts +14 -0
  9. package/lib/cdp/launcher.js +102 -0
  10. package/lib/cdp/tabs.d.ts +6 -0
  11. package/lib/cdp/tabs.js +10 -0
  12. package/lib/cli/args.d.ts +14 -0
  13. package/lib/cli/args.js +19 -0
  14. package/lib/cli/commands.d.ts +1 -0
  15. package/lib/cli/commands.js +19 -0
  16. package/lib/cli.d.ts +1 -0
  17. package/lib/cli.js +7 -0
  18. package/lib/config/store.d.ts +45 -0
  19. package/lib/config/store.js +46 -0
  20. package/lib/index.d.ts +3 -0
  21. package/lib/index.js +7 -0
  22. package/lib/logger.d.ts +2 -0
  23. package/lib/logger.js +7 -0
  24. package/lib/mcp/api/operations.gen.d.ts +152 -0
  25. package/lib/mcp/api/operations.gen.js +2300 -0
  26. package/lib/mcp/api/tools.d.ts +3 -0
  27. package/lib/mcp/api/tools.js +36 -0
  28. package/lib/mcp/remap_errors.d.ts +2 -0
  29. package/lib/mcp/remap_errors.js +27 -0
  30. package/lib/mcp/server.d.ts +38 -0
  31. package/lib/mcp/server.js +76 -0
  32. package/lib/mcp/start.d.ts +1 -0
  33. package/lib/mcp/start.js +63 -0
  34. package/lib/mcp/tools/agent/analyze.d.ts +3 -0
  35. package/lib/mcp/tools/agent/analyze.js +26 -0
  36. package/lib/mcp/tools/agent/attach.d.ts +11 -0
  37. package/lib/mcp/tools/agent/attach.js +72 -0
  38. package/lib/mcp/tools/agent/build.d.ts +33 -0
  39. package/lib/mcp/tools/agent/build.js +74 -0
  40. package/lib/mcp/tools/agent/capture.d.ts +10 -0
  41. package/lib/mcp/tools/agent/capture.js +142 -0
  42. package/lib/mcp/tools/agent/eval.d.ts +5 -0
  43. package/lib/mcp/tools/agent/eval.js +127 -0
  44. package/lib/mcp/tools/agent/find.d.ts +3 -0
  45. package/lib/mcp/tools/agent/find.js +60 -0
  46. package/lib/mcp/tools/agent/proof.d.ts +3 -0
  47. package/lib/mcp/tools/agent/proof.js +214 -0
  48. package/lib/mcp/tools/agent/propose.d.ts +9 -0
  49. package/lib/mcp/tools/agent/propose.js +75 -0
  50. package/lib/mcp/tools/agent/publish.d.ts +3 -0
  51. package/lib/mcp/tools/agent/publish.js +122 -0
  52. package/lib/mcp/tools/agent/replay.d.ts +3 -0
  53. package/lib/mcp/tools/agent/replay.js +46 -0
  54. package/lib/mcp/tools/agent/session.d.ts +10 -0
  55. package/lib/mcp/tools/agent/session.js +29 -0
  56. package/lib/mcp/tools/credentials/authenticate.d.ts +3 -0
  57. package/lib/mcp/tools/credentials/authenticate.js +108 -0
  58. package/lib/mcp/tools/credentials/build.d.ts +16 -0
  59. package/lib/mcp/tools/credentials/build.js +33 -0
  60. package/lib/mcp/tools/credentials/cache.d.ts +70 -0
  61. package/lib/mcp/tools/credentials/cache.js +114 -0
  62. package/lib/mcp/tools/credentials/env-file.d.ts +25 -0
  63. package/lib/mcp/tools/credentials/env-file.js +71 -0
  64. package/lib/mcp/tools/credentials/import.d.ts +2 -0
  65. package/lib/mcp/tools/credentials/import.js +52 -0
  66. package/lib/mcp/tools/credentials/issue.d.ts +2 -0
  67. package/lib/mcp/tools/credentials/issue.js +84 -0
  68. package/lib/mcp/tools/credentials/list-cached.d.ts +2 -0
  69. package/lib/mcp/tools/credentials/list-cached.js +55 -0
  70. package/lib/mcp/tools/credentials/register.d.ts +18 -0
  71. package/lib/mcp/tools/credentials/register.js +30 -0
  72. package/lib/mcp/tools/credentials/resolve-owner-key.d.ts +12 -0
  73. package/lib/mcp/tools/credentials/resolve-owner-key.js +68 -0
  74. package/lib/mcp/tools/credentials/session-cache.d.ts +14 -0
  75. package/lib/mcp/tools/credentials/session-cache.js +19 -0
  76. package/lib/mcp/tools/credentials/status.d.ts +3 -0
  77. package/lib/mcp/tools/credentials/status.js +62 -0
  78. package/lib/mcp-bin.d.ts +1 -0
  79. package/lib/mcp-bin.js +7 -0
  80. package/lib/old/client.d.ts +67 -0
  81. package/lib/old/client.js +146 -0
  82. package/lib/old/flag.d.ts +11 -0
  83. package/lib/old/flag.js +13 -0
  84. package/lib/old/identity.d.ts +22 -0
  85. package/lib/old/identity.js +47 -0
  86. package/lib/old/instructions.d.ts +7 -0
  87. package/lib/old/instructions.js +29 -0
  88. package/lib/old/login-cdp.d.ts +23 -0
  89. package/lib/old/login-cdp.js +125 -0
  90. package/lib/old/to-register.d.ts +47 -0
  91. package/lib/old/to-register.js +133 -0
  92. package/lib/old/tools/authenticate.d.ts +18 -0
  93. package/lib/old/tools/authenticate.js +127 -0
  94. package/lib/old/tools/build.d.ts +9 -0
  95. package/lib/old/tools/build.js +16 -0
  96. package/lib/old/tools/me-providers.d.ts +10 -0
  97. package/lib/old/tools/me-providers.js +32 -0
  98. package/lib/old/tools/publish.d.ts +16 -0
  99. package/lib/old/tools/publish.js +97 -0
  100. package/lib/paths.d.ts +11 -0
  101. package/lib/paths.js +32 -0
  102. package/lib/pkg.d.ts +3 -0
  103. package/lib/pkg.js +13 -0
  104. package/lib/proof/attestor.d.ts +39 -0
  105. package/lib/proof/attestor.js +207 -0
  106. package/lib/proof/replay.d.ts +8 -0
  107. package/lib/proof/replay.js +73 -0
  108. package/lib/provider/constraints.d.ts +2 -0
  109. package/lib/provider/constraints.js +122 -0
  110. package/lib/provider/draft.d.ts +6 -0
  111. package/lib/provider/draft.js +68 -0
  112. package/lib/provider/graphql.d.ts +2 -0
  113. package/lib/provider/graphql.js +35 -0
  114. package/lib/provider/matcher.d.ts +36 -0
  115. package/lib/provider/matcher.js +436 -0
  116. package/lib/provider/normalize.d.ts +4 -0
  117. package/lib/provider/normalize.js +51 -0
  118. package/lib/provider/schema.d.ts +76 -0
  119. package/lib/provider/schema.js +1 -0
  120. package/lib/provider/secrets.d.ts +5 -0
  121. package/lib/provider/secrets.js +22 -0
  122. package/lib/provider/to-version.d.ts +22 -0
  123. package/lib/provider/to-version.js +68 -0
  124. package/lib/types/openapi.d.ts +4 -0
  125. package/lib/types/openapi.js +1 -0
  126. package/package.json +88 -0
  127. package/scripts/download-zk-files.ts +42 -0
  128. package/scripts/prepend-shebang.ts +17 -0
  129. package/skill/SKILL.md +248 -0
  130. package/src/cdp/capture.ts +215 -0
  131. package/src/cdp/client.ts +20 -0
  132. package/src/cdp/eviction.ts +52 -0
  133. package/src/cdp/launcher.ts +133 -0
  134. package/src/cdp/tabs.ts +22 -0
  135. package/src/cli/args.ts +20 -0
  136. package/src/cli/commands.ts +27 -0
  137. package/src/cli.ts +7 -0
  138. package/src/config/store.ts +86 -0
  139. package/src/index.ts +12 -0
  140. package/src/logger.ts +8 -0
  141. package/src/mcp/api/operations.gen.ts +2397 -0
  142. package/src/mcp/api/tools.ts +45 -0
  143. package/src/mcp/remap_errors.ts +35 -0
  144. package/src/mcp/server.ts +119 -0
  145. package/src/mcp/start.ts +78 -0
  146. package/src/mcp/tools/agent/analyze.ts +34 -0
  147. package/src/mcp/tools/agent/attach.ts +95 -0
  148. package/src/mcp/tools/agent/build.ts +99 -0
  149. package/src/mcp/tools/agent/capture.ts +202 -0
  150. package/src/mcp/tools/agent/eval.ts +161 -0
  151. package/src/mcp/tools/agent/find.ts +73 -0
  152. package/src/mcp/tools/agent/proof.ts +272 -0
  153. package/src/mcp/tools/agent/propose.ts +111 -0
  154. package/src/mcp/tools/agent/publish.ts +155 -0
  155. package/src/mcp/tools/agent/replay.ts +58 -0
  156. package/src/mcp/tools/agent/session.ts +46 -0
  157. package/src/mcp/tools/credentials/authenticate.ts +135 -0
  158. package/src/mcp/tools/credentials/build.ts +37 -0
  159. package/src/mcp/tools/credentials/cache.ts +161 -0
  160. package/src/mcp/tools/credentials/env-file.ts +101 -0
  161. package/src/mcp/tools/credentials/import.ts +72 -0
  162. package/src/mcp/tools/credentials/issue.ts +107 -0
  163. package/src/mcp/tools/credentials/list-cached.ts +66 -0
  164. package/src/mcp/tools/credentials/register.ts +49 -0
  165. package/src/mcp/tools/credentials/resolve-owner-key.ts +78 -0
  166. package/src/mcp/tools/credentials/session-cache.ts +31 -0
  167. package/src/mcp/tools/credentials/status.ts +82 -0
  168. package/src/mcp-bin.ts +7 -0
  169. package/src/old/client.ts +236 -0
  170. package/src/old/flag.ts +13 -0
  171. package/src/old/identity.ts +56 -0
  172. package/src/old/instructions.ts +30 -0
  173. package/src/old/login-cdp.ts +153 -0
  174. package/src/old/to-register.ts +234 -0
  175. package/src/old/tools/authenticate.ts +152 -0
  176. package/src/old/tools/build.ts +21 -0
  177. package/src/old/tools/me-providers.ts +42 -0
  178. package/src/old/tools/publish.ts +135 -0
  179. package/src/paths.ts +37 -0
  180. package/src/pkg.ts +24 -0
  181. package/src/proof/attestor.ts +286 -0
  182. package/src/proof/replay.ts +102 -0
  183. package/src/provider/constraints.ts +150 -0
  184. package/src/provider/draft.ts +105 -0
  185. package/src/provider/graphql.ts +42 -0
  186. package/src/provider/matcher.ts +616 -0
  187. package/src/provider/normalize.ts +65 -0
  188. package/src/provider/schema.ts +92 -0
  189. package/src/provider/secrets.ts +29 -0
  190. package/src/provider/to-version.ts +90 -0
  191. package/src/types/openapi.ts +7 -0
@@ -0,0 +1,161 @@
1
+ import { deriveFromPrivateKey } from '@reclaimprotocol/client'
2
+ import { ConfigStore } from '../../../config/store.ts'
3
+ import { findProjectRoot } from '../../../paths.ts'
4
+ import { readPrivateKeyFromEnvFile } from './env-file.ts'
5
+
6
+ export type CachedCredential = {
7
+ address: string
8
+ privateKey: string
9
+ label?: string
10
+ orgId?: string
11
+ createdAt: string
12
+ /** Project dir where this credential was first created. */
13
+ createdIn: string
14
+ /** All project dirs this credential has been used in ("used by"). */
15
+ projectDirs: string[]
16
+ }
17
+
18
+ type CacheFile = {
19
+ version: 1
20
+ credentials: CachedCredential[]
21
+ }
22
+
23
+ const EMPTY: CacheFile = { version: 1, credentials: [] }
24
+
25
+ /**
26
+ * Read the `credentials` section of the unified config. Returns an empty
27
+ * doc if missing or malformed — we'd rather lose the cache than crash the
28
+ * CLI on a corrupted file the user can re-create.
29
+ */
30
+ export function readCredentialsFromCache(): CacheFile {
31
+ const creds = new ConfigStore().read()?.credentials
32
+ if(!Array.isArray(creds)) {
33
+ return { ...EMPTY }
34
+ }
35
+
36
+ return { version: 1, credentials: creds }
37
+ }
38
+
39
+ /**
40
+ * Persist the `credentials` section, merging into the unified config so
41
+ * other sections (browser_agent, session) survive untouched. We rewrite
42
+ * the whole list each save; it's tiny and the alternative (append-only)
43
+ * loses our ability to deduplicate `projectDirs`.
44
+ */
45
+ export function writeCache(doc: CacheFile) {
46
+ new ConfigStore().write({ credentials: doc.credentials })
47
+ }
48
+
49
+ /**
50
+ * Upsert a credential entry. If a row already exists for `address`,
51
+ * we merge the new `projectDirs` into the existing list, refresh
52
+ * mutable fields, and keep the original `createdAt` / `createdIn`
53
+ * (backfilling `createdIn` from the new entry when an old row lacks it).
54
+ */
55
+ export function upsertCredential(entry: CachedCredential) {
56
+ const doc = readCredentialsFromCache()
57
+ const idx = doc.credentials.findIndex(
58
+ (c) => c.address === entry.address.toLowerCase(),
59
+ )
60
+ const merged: CachedCredential = {
61
+ ...entry,
62
+ address: entry.address.toLowerCase(),
63
+ projectDirs: Array.from(
64
+ new Set([
65
+ ...(doc.credentials[idx]?.projectDirs ?? []),
66
+ ...entry.projectDirs,
67
+ ]),
68
+ ),
69
+ createdAt: doc.credentials[idx]?.createdAt ?? entry.createdAt,
70
+ createdIn: doc.credentials[idx]?.createdIn ?? entry.createdIn,
71
+ }
72
+ if(idx >= 0) {
73
+ doc.credentials[idx] = merged
74
+ } else {
75
+ doc.credentials.unshift(merged)
76
+ }
77
+
78
+ writeCache(doc)
79
+ return merged
80
+ }
81
+
82
+ /** Look up a cached credential by address. Case-insensitive. */
83
+ export function findCredentialFromCachedBy(
84
+ address: string
85
+ ): CachedCredential | undefined {
86
+ const doc = readCredentialsFromCache()
87
+ const lc = address.toLowerCase()
88
+ return doc.credentials.find((c) => c.address === lc)
89
+ }
90
+
91
+ export type SuggestionReason = 'createdIn' | 'usedIn' | 'newest'
92
+
93
+ export type CredentialSuggestion = {
94
+ credential: CachedCredential
95
+ reason: SuggestionReason
96
+ }
97
+
98
+ /**
99
+ * Pick the best cached credential for `dir`. Ranking tiers:
100
+ * 1. created in `dir` (reason: 'createdIn')
101
+ * 2. previously used in `dir` (reason: 'usedIn')
102
+ * 3. most recently created (reason: 'newest')
103
+ * Within every tier, newest `createdAt` wins; address ascending is the
104
+ * final deterministic tie-break. Returns undefined only on an empty cache.
105
+ *
106
+ * Pass a pre-read `creds` array to avoid a redundant `readCache()` (and to
107
+ * rank against the same snapshot the caller already holds).
108
+ */
109
+ export function suggestCredentialForDir(
110
+ dir: string,
111
+ creds: CachedCredential[] = readCredentialsFromCache().credentials,
112
+ ): CredentialSuggestion | undefined {
113
+ if(creds.length === 0) {
114
+ return undefined
115
+ }
116
+
117
+ const byNewest = (a: CachedCredential, b: CachedCredential) => {
118
+ return (b.createdAt ?? '').localeCompare(a.createdAt ?? '') ||
119
+ a.address.localeCompare(b.address)
120
+ }
121
+
122
+ const createdHere = creds.filter((c) => c.createdIn === dir).sort(byNewest)
123
+ if(createdHere[0]) {
124
+ return { credential: createdHere[0], reason: 'createdIn' }
125
+ }
126
+
127
+ const usedHere = creds
128
+ .filter((c) => c.projectDirs.includes(dir))
129
+ .sort(byNewest)
130
+ if(usedHere[0]) {
131
+ return { credential: usedHere[0], reason: 'usedIn' }
132
+ }
133
+
134
+ return { credential: [...creds].sort(byNewest)[0], reason: 'newest' }
135
+ }
136
+
137
+ /**
138
+ * Resolve the private key to sign attestor proofs with (the claim
139
+ * "owner"). Reuses the app credential rather than a dedicated key — for
140
+ * build-time test proofs the developer is both the app and the proof
141
+ * owner, and the owner identity is immaterial to whether the provider's
142
+ * matching works.
143
+ *
144
+ * The owner is chosen up front (see the `resolve_owner_key` tool) and
145
+ * passed by `address`; this just fetches the matching private key:
146
+ * - the `RECLAIM_PRIVATE_KEY` env / project `.env` key, if it derives
147
+ * to `address` (the dev pinned it), else
148
+ * - the cached credential with that address.
149
+ * Returns undefined when no key for `address` is known.
150
+ */
151
+ export function resolveOwnerPrivateKey(address: string): string | undefined {
152
+ const target = address.toLowerCase()
153
+ const envKey =
154
+ process.env.RECLAIM_PRIVATE_KEY ??
155
+ readPrivateKeyFromEnvFile(findProjectRoot(process.cwd()))
156
+ if(envKey && deriveFromPrivateKey(envKey).address === target) {
157
+ return envKey
158
+ }
159
+
160
+ return findCredentialFromCachedBy(target)?.privateKey
161
+ }
@@ -0,0 +1,101 @@
1
+ import {
2
+ appendFileSync,
3
+ chmodSync,
4
+ existsSync,
5
+ readFileSync,
6
+ writeFileSync,
7
+ } from 'node:fs'
8
+ import { join } from 'node:path'
9
+
10
+ const PRIVATE_KEY = 'RECLAIM_PRIVATE_KEY'
11
+ const GITIGNORE_LINE = '.env'
12
+
13
+ export type EnvWriteResult = {
14
+ envPath: string
15
+ gitignorePath: string
16
+ gitignoreAppended: boolean
17
+ }
18
+
19
+ /**
20
+ * Idempotently set `RECLAIM_PRIVATE_KEY` in the project's `.env`, then
21
+ * `chmod 0600` it. The private key is the only secret we write — the
22
+ * ETH address (and public key) are derived from it on demand, so there
23
+ * is no separate address variable to keep in sync. Existing variables
24
+ * are replaced in place; other lines are preserved verbatim so we
25
+ * don't trample on unrelated config the user wrote by hand.
26
+ *
27
+ * Also appends `.env` to the project's `.gitignore` if it isn't
28
+ * already listed.
29
+ */
30
+ export function writeAppCredentialsToEnv(
31
+ projectDir: string,
32
+ privateKey: string,
33
+ ): EnvWriteResult {
34
+ const envPath = join(projectDir, '.env')
35
+ const lines = existsSync(envPath)
36
+ ? readFileSync(envPath, 'utf8').split('\n')
37
+ : []
38
+
39
+ setOrInsert(lines, PRIVATE_KEY, privateKey)
40
+
41
+ const body = lines.join('\n').replace(/\n+$/, '') + '\n'
42
+ writeFileSync(envPath, body, { mode: 0o600 })
43
+ chmodSync(envPath, 0o600)
44
+
45
+ const gitignorePath = join(projectDir, '.gitignore')
46
+ const gitignoreAppended = ensureLine(gitignorePath, GITIGNORE_LINE)
47
+
48
+ return { envPath, gitignorePath, gitignoreAppended }
49
+ }
50
+
51
+ /**
52
+ * Read `RECLAIM_PRIVATE_KEY` back out of the project's `.env`, if set.
53
+ * Read live (not from `process.env`) so a credential written by
54
+ * `issue_credentials` / `import_credentials` is picked up immediately,
55
+ * without a Claude Code restart. Returns undefined when the file or the
56
+ * variable is absent.
57
+ */
58
+ export function readPrivateKeyFromEnvFile(
59
+ projectDir: string,
60
+ ): string | undefined {
61
+ const envPath = join(projectDir, '.env')
62
+ if(!existsSync(envPath)) {
63
+ return undefined
64
+ }
65
+
66
+ const prefix = `${PRIVATE_KEY}=`
67
+ const line = readFileSync(envPath, 'utf8')
68
+ .split('\n')
69
+ .find((l) => l.startsWith(prefix))
70
+ const value = line?.slice(prefix.length).trim()
71
+ return value || undefined
72
+ }
73
+
74
+ function setOrInsert(lines: string[], key: string, value: string) {
75
+ const prefix = `${key}=`
76
+ const idx = lines.findIndex((l) => l.startsWith(prefix))
77
+ const next = `${key}=${value}`
78
+ if(idx >= 0) {
79
+ lines[idx] = next
80
+ return
81
+ }
82
+
83
+ lines.push(next)
84
+ }
85
+
86
+ function ensureLine(path: string, line: string): boolean {
87
+ if(!existsSync(path)) {
88
+ writeFileSync(path, line + '\n', { mode: 0o644 })
89
+ return true
90
+ }
91
+
92
+ const body = readFileSync(path, 'utf8')
93
+ const present = body.split('\n').some((l) => l.trim() === line)
94
+ if(present) {
95
+ return false
96
+ }
97
+
98
+ const suffix = body.endsWith('\n') ? '' : '\n'
99
+ appendFileSync(path, suffix + line + '\n')
100
+ return true
101
+ }
@@ -0,0 +1,72 @@
1
+ import { deriveFromPrivateKey } from '@reclaimprotocol/client'
2
+ import { findProjectRoot } from '../../../paths.ts'
3
+ import { defineTool, type RegisteredTool } from '../../server.ts'
4
+ import { persistCredentialLocally } from './register.ts'
5
+
6
+ export function importTool(): RegisteredTool {
7
+ return defineTool<{
8
+ privateKey: string
9
+ label?: string
10
+ projectDir?: string
11
+ writeEnv?: boolean
12
+ }>(
13
+ {
14
+ name: 'import_credentials',
15
+ description:
16
+ 'Import an existing Reclaim app credential the user ' +
17
+ 'already holds elsewhere. Provide the private key ' +
18
+ '(0x-prefixed hex, 64 chars); we derive the address ' +
19
+ 'locally and cache it. By default also writes ' +
20
+ 'RECLAIM_PRIVATE_KEY to the project .env (0600) and appends ' +
21
+ '.env to .gitignore; pass `writeEnv: false` to keep it in ' +
22
+ 'the local cache (memory) only and leave .env untouched. ' +
23
+ 'ALWAYS ask the dev whether to save to .env before writing. ' +
24
+ 'The credential stays local-only (SANDBOX); link it to an ' +
25
+ 'org to use it in production.',
26
+ inputSchema: {
27
+ type: 'object',
28
+ properties: {
29
+ privateKey: {
30
+ type: 'string',
31
+ pattern: '^0x[0-9a-fA-F]{64}$',
32
+ description:
33
+ '0x-prefixed hex private key (64 hex chars / 32 bytes).',
34
+ },
35
+ label: {
36
+ type: 'string',
37
+ minLength: 1,
38
+ maxLength: 100,
39
+ },
40
+ projectDir: {
41
+ type: 'string',
42
+ description:
43
+ 'Project directory to write .env into. Defaults ' +
44
+ 'to the closest ancestor of cwd containing ' +
45
+ '.git or package.json.',
46
+ },
47
+ writeEnv: {
48
+ type: 'boolean',
49
+ description:
50
+ 'Write RECLAIM_PRIVATE_KEY to the project .env ' +
51
+ '(default true). Set false to cache in memory only, ' +
52
+ 'without touching .env.',
53
+ },
54
+ },
55
+ required: ['privateKey'],
56
+ },
57
+ },
58
+ async(args) => {
59
+ const keypair = deriveFromPrivateKey(args.privateKey)
60
+ const projectDir = findProjectRoot(args.projectDir || process.cwd())
61
+
62
+ const persisted = persistCredentialLocally(
63
+ keypair,
64
+ args.label,
65
+ projectDir,
66
+ args.writeEnv ?? true,
67
+ )
68
+
69
+ return { ...persisted }
70
+ },
71
+ )
72
+ }
@@ -0,0 +1,107 @@
1
+ import { generateKeypair } from '@reclaimprotocol/client'
2
+ import { findProjectRoot } from '../../../paths.ts'
3
+ import { defineTool, type RegisteredTool } from '../../server.ts'
4
+ import { findCredentialFromCachedBy } from './cache.ts'
5
+ import { persistCredentialLocally } from './register.ts'
6
+
7
+ export function issueTool(): RegisteredTool {
8
+ return defineTool<{
9
+ reuseCachedAppPk?: string
10
+ label?: string
11
+ projectDir?: string
12
+ writeEnv?: boolean
13
+ }>(
14
+ {
15
+ name: 'issue_credentials',
16
+ description:
17
+ 'Mint a fresh anonymous Reclaim app key-pair (or reuse ' +
18
+ 'a cached one via `reuseCachedAppPk`) and cache it locally. ' +
19
+ 'By default also writes RECLAIM_PRIVATE_KEY to the project ' +
20
+ '.env (mode 0600) and appends .env to .gitignore; pass ' +
21
+ '`writeEnv: false` to keep it in the local cache (memory) ' +
22
+ 'only and leave .env untouched. ALWAYS ask the dev whether ' +
23
+ 'to save to .env before writing it. The key-pair is ' +
24
+ 'generated locally and is NOT registered with the server — ' +
25
+ 'anonymous credentials operate in SANDBOX mode entirely ' +
26
+ 'client-side. PREFER REUSE over generating a new credential ' +
27
+ 'unless the user explicitly asks for a new app identity. To ' +
28
+ 'use a credential for verification in production, link it to ' +
29
+ 'an organization (via link_credential) and add billing.',
30
+ inputSchema: {
31
+ type: 'object',
32
+ properties: {
33
+ reuseCachedAppPk: {
34
+ type: 'string',
35
+ pattern: '^0x[0-9a-fA-F]{40}$',
36
+ description:
37
+ 'Lowercase ETH address of a cached credential ' +
38
+ 'to reuse instead of minting a new one.',
39
+ },
40
+ label: {
41
+ type: 'string',
42
+ minLength: 1,
43
+ maxLength: 100,
44
+ description: 'Human label stored in the local cache.',
45
+ },
46
+ projectDir: {
47
+ type: 'string',
48
+ description:
49
+ 'Project directory to write .env into. Defaults ' +
50
+ 'to the closest ancestor of cwd containing ' +
51
+ '.git or package.json.',
52
+ },
53
+ writeEnv: {
54
+ type: 'boolean',
55
+ description:
56
+ 'Write RECLAIM_PRIVATE_KEY to the project .env ' +
57
+ '(default true). Set false to cache the credential ' +
58
+ 'in memory only, without touching .env.',
59
+ },
60
+ },
61
+ },
62
+ },
63
+ async(args) => {
64
+ const projectDir = findProjectRoot(args.projectDir || process.cwd())
65
+ let keypair: { address: string, privateKey: string } | undefined
66
+ let reused = false
67
+
68
+ if(args.reuseCachedAppPk) {
69
+ const cached = findCredentialFromCachedBy(args.reuseCachedAppPk)
70
+ if(!cached) {
71
+ throw new Error(
72
+ `No cached credential for ${args.reuseCachedAppPk}. ` +
73
+ 'Call list_cached_credentials first.',
74
+ )
75
+ }
76
+
77
+ keypair = {
78
+ address: cached.address,
79
+ privateKey: cached.privateKey,
80
+ }
81
+ reused = true
82
+ } else {
83
+ keypair = generateKeypair()
84
+ }
85
+
86
+ const writeEnv = args.writeEnv ?? true
87
+ const persisted = persistCredentialLocally(
88
+ keypair,
89
+ args.label,
90
+ projectDir,
91
+ writeEnv,
92
+ )
93
+
94
+ return {
95
+ reused,
96
+ ...persisted,
97
+ note: writeEnv
98
+ ? 'Private key written to .env (and the local cache); it is ' +
99
+ 'NOT in this response on purpose. The credential is ' +
100
+ 'local-only (SANDBOX); link it to an org for production.'
101
+ : 'Private key cached in memory only (.env untouched). Sign ' +
102
+ 'proofs with it by passing run_proof({ ownerAddress }). The ' +
103
+ 'credential is local-only (SANDBOX).',
104
+ }
105
+ },
106
+ )
107
+ }
@@ -0,0 +1,66 @@
1
+ import { findProjectRoot } from '../../../paths.ts'
2
+ import { defineTool, type RegisteredTool } from '../../server.ts'
3
+ import { readCredentialsFromCache, suggestCredentialForDir } from './cache.ts'
4
+
5
+ export function listCachedTool(): RegisteredTool {
6
+ return defineTool<Record<string, never>>(
7
+ {
8
+ name: 'list_cached_credentials',
9
+ description:
10
+ 'List Reclaim app credentials (ETH key-pairs) cached ' +
11
+ 'locally in ~/.reclaim/config.json. Returns one ' +
12
+ 'row per credential with address, optional label and ' +
13
+ 'orgId, createdAt, createdIn (where it was first created), ' +
14
+ 'and the project directories that have used it. Private keys ' +
15
+ 'are stripped. The response also reports `currentDir` and ' +
16
+ 'flags the best credential for it (`suggested`, plus ' +
17
+ '`suggestedAddress`/`suggestionReason`) — prefer reusing that ' +
18
+ 'one over minting a new credential. When the cache is empty a ' +
19
+ '`suggestion` string explains how to create one.',
20
+ inputSchema: {
21
+ type: 'object',
22
+ properties: {},
23
+ },
24
+ },
25
+ async() => {
26
+ const doc = readCredentialsFromCache()
27
+ const currentDir = findProjectRoot(process.cwd())
28
+ const suggestion = suggestCredentialForDir(currentDir, doc.credentials)
29
+ const suggestedAddress = suggestion?.credential.address
30
+
31
+ const items = doc.credentials.map((c) => ({
32
+ address: c.address,
33
+ label: c.label,
34
+ orgId: c.orgId,
35
+ createdAt: c.createdAt,
36
+ createdIn: c.createdIn,
37
+ projectDirs: c.projectDirs,
38
+ matchesCurrentDir:
39
+ c.createdIn === currentDir || c.projectDirs.includes(currentDir),
40
+ suggested: c.address === suggestedAddress,
41
+ }))
42
+
43
+ // Promote the single suggested row to the top; stable sort keeps
44
+ // the rest in insertion (newest-first) order.
45
+ items.sort((a, b) => Number(b.suggested) - Number(a.suggested))
46
+
47
+ if(items.length === 0) {
48
+ return {
49
+ items,
50
+ currentDir,
51
+ suggestion:
52
+ 'No cached credentials. Call issue_credentials to mint a new ' +
53
+ 'app key-pair, or import_credentials if you already hold a ' +
54
+ 'private key. Do not invent credentials.',
55
+ }
56
+ }
57
+
58
+ return {
59
+ items,
60
+ currentDir,
61
+ suggestedAddress,
62
+ suggestionReason: suggestion?.reason,
63
+ }
64
+ },
65
+ )
66
+ }
@@ -0,0 +1,49 @@
1
+ import type { EthKeypair } from '@reclaimprotocol/client'
2
+ import { type CachedCredential, upsertCredential } from './cache.ts'
3
+ import { type EnvWriteResult, writeAppCredentialsToEnv } from './env-file.ts'
4
+
5
+ /** Shape returned by `persistCredentialLocally` — the fields both
6
+ * `issue_credentials` and `import_credentials` expose in their MCP
7
+ * responses. The env-file fields are present only when the credential
8
+ * was persisted to `.env` (`writeEnv: true`); a memory-only credential
9
+ * lives in the cache and omits them. */
10
+ export type PersistedCredential = Pick<
11
+ CachedCredential,
12
+ 'address' | 'label'
13
+ > &
14
+ Partial<EnvWriteResult> & { wroteEnv: boolean }
15
+
16
+ /** Local persistence step shared by `issue_credentials` and
17
+ * `import_credentials`. Always upserts the credentials cache; when
18
+ * `writeEnv` is true (default) it also writes `RECLAIM_PRIVATE_KEY` to
19
+ * the project `.env` (0600) and appends `.env` to `.gitignore`. Pass
20
+ * `writeEnv: false` to keep the credential in the cache (memory) only,
21
+ * without touching the project's `.env`. */
22
+ export function persistCredentialLocally(
23
+ keypair: EthKeypair,
24
+ label: string | undefined,
25
+ projectDir: string,
26
+ writeEnv = true,
27
+ ): PersistedCredential {
28
+ const cached = upsertCredential({
29
+ address: keypair.address,
30
+ privateKey: keypair.privateKey,
31
+ label,
32
+ createdAt: new Date().toISOString(),
33
+ createdIn: projectDir,
34
+ projectDirs: [projectDir],
35
+ })
36
+ if(!writeEnv) {
37
+ return { address: cached.address, label: cached.label, wroteEnv: false }
38
+ }
39
+
40
+ const env = writeAppCredentialsToEnv(projectDir, keypair.privateKey)
41
+ return {
42
+ address: cached.address,
43
+ label: cached.label,
44
+ wroteEnv: true,
45
+ envPath: env.envPath,
46
+ gitignorePath: env.gitignorePath,
47
+ gitignoreAppended: env.gitignoreAppended,
48
+ }
49
+ }
@@ -0,0 +1,78 @@
1
+ import { deriveFromPrivateKey } from '@reclaimprotocol/client'
2
+ import { findProjectRoot } from '../../../paths.ts'
3
+ import { defineTool, type RegisteredTool } from '../../server.ts'
4
+ import { suggestCredentialForDir } from './cache.ts'
5
+ import { readPrivateKeyFromEnvFile } from './env-file.ts'
6
+
7
+ /**
8
+ * Decide which app credential `run_proof` should sign with, BEFORE
9
+ * calling it. `run_proof` requires an `ownerAddress` and does no
10
+ * resolution of its own — this is the single place that "checks memory,
11
+ * env, etc and offers options".
12
+ *
13
+ * Checks, in order: `RECLAIM_PRIVATE_KEY` in the MCP env → the project
14
+ * `.env` → the best cached credential for this directory. Never returns
15
+ * the private key — only the address to pass through as `ownerAddress`.
16
+ */
17
+ export function resolveOwnerKeyTool(): RegisteredTool {
18
+ return defineTool<Record<string, never>>(
19
+ {
20
+ name: 'resolve_owner_key',
21
+ description:
22
+ 'Resolve which app credential run_proof should sign with — ' +
23
+ 'call this BEFORE run_proof. Checks, in order: ' +
24
+ 'RECLAIM_PRIVATE_KEY in the MCP env, RECLAIM_PRIVATE_KEY in the ' +
25
+ 'project .env, then the best cached credential for this ' +
26
+ 'directory. Returns { ready: true, address, source } when one ' +
27
+ 'is available — pass `address` to run_proof as `ownerAddress`. ' +
28
+ 'Returns { ready: false } when none exists: then ask the dev to ' +
29
+ 'provide a private key (import_credentials) or create one ' +
30
+ '(issue_credentials) — optionally with writeEnv:false to keep ' +
31
+ 'it in the local cache (memory) only — and call resolve_owner_key ' +
32
+ 'again. Never exposes the private key.',
33
+ inputSchema: { type: 'object', properties: {} },
34
+ },
35
+ async() => {
36
+ const dir = findProjectRoot(process.cwd())
37
+
38
+ const envKey = process.env.RECLAIM_PRIVATE_KEY
39
+ if(envKey) {
40
+ return {
41
+ ready: true,
42
+ address: deriveFromPrivateKey(envKey).address,
43
+ source: 'env',
44
+ }
45
+ }
46
+
47
+ const dotenvKey = readPrivateKeyFromEnvFile(dir)
48
+ if(dotenvKey) {
49
+ return {
50
+ ready: true,
51
+ address: deriveFromPrivateKey(dotenvKey).address,
52
+ source: 'dotenv',
53
+ }
54
+ }
55
+
56
+ const suggestion = suggestCredentialForDir(dir)
57
+ if(suggestion) {
58
+ return {
59
+ ready: true,
60
+ address: suggestion.credential.address,
61
+ source: 'cache',
62
+ reason: suggestion.reason,
63
+ }
64
+ }
65
+
66
+ return {
67
+ ready: false,
68
+ message:
69
+ 'No app credential available (no RECLAIM_PRIVATE_KEY in env ' +
70
+ 'or .env, and the cache is empty). Ask the dev to provide ' +
71
+ 'their private key via import_credentials, or create one via ' +
72
+ 'issue_credentials (pass writeEnv:false to keep it out of ' +
73
+ '.env). Then call resolve_owner_key again and pass its ' +
74
+ 'address to run_proof as ownerAddress.',
75
+ }
76
+ },
77
+ )
78
+ }
@@ -0,0 +1,31 @@
1
+ import { ConfigStore } from '../../../config/store.ts'
2
+
3
+ export type CachedSession = {
4
+ token: string
5
+ /** ISO timestamp when the token expires (2 days from issue). */
6
+ expiresAt: string
7
+ /** Email of the user who authorized (if available). */
8
+ email?: string
9
+ }
10
+
11
+ /** Persist the auth token to the `session` section of the unified config. */
12
+ export function writeSession(session: CachedSession) {
13
+ new ConfigStore().write({ session })
14
+ }
15
+
16
+ /**
17
+ * Read the cached session, returning it only if the token is still
18
+ * valid (not yet expired). Returns undefined on any error.
19
+ */
20
+ export function readSession(): CachedSession | undefined {
21
+ const s = new ConfigStore().read()?.session
22
+ if(!s?.token || !s.expiresAt) {
23
+ return undefined
24
+ }
25
+
26
+ if(new Date(s.expiresAt) <= new Date()) {
27
+ return undefined // expired
28
+ }
29
+
30
+ return s
31
+ }