paperclip-plugin-claude-accounts 0.3.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.
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # Claude Accounts (Paperclip plugin)
2
+
3
+ Every team member connects **their own** Claude Pro/Max account from the browser, picks which agents use it,
4
+ and it **stays signed in** across restarts, redeploys and reconnects. No `docker exec … /bin/sh`, no `claude login`
5
+ every few hours.
6
+
7
+ ## Why you kept having to re-auth
8
+
9
+ With the "local" Claude sign-in, Paperclip saves only `claudeAiOauth.accessToken`, which is short-lived. It drops the
10
+ refresh token (`server/src/services/local-ai-credentials.ts`) and never writes a renewed token back for Anthropic.
11
+ So the saved login dies and you're sent back to a shell. The UI setup-token flow only works in sandbox
12
+ environments, not local/SSH (Docker).
13
+
14
+ This plugin keeps the **full** login, including the refresh token, in a per-user folder. The Claude CLI renews it there on its own,
15
+ and the plugin backs it up so nothing is lost when the container restarts.
16
+
17
+ ## Paperclip's own "Connect Claude", without the terminal
18
+
19
+ Paperclip normally tells you to run this in a shell:
20
+
21
+ ```
22
+ (export CLAUDE_CONFIG_DIR='/paperclip/instances/default/ai-local-logins/<id>' && mkdir -p "$CLAUDE_CONFIG_DIR" && claude auth login)
23
+ ```
24
+
25
+ The plugin does the same thing for you:
26
+
27
+ - **One click:** on **Claude accounts**, click **Connect to Paperclip**. The plugin:
28
+ 1. starts Paperclip's sign-in attempt
29
+ 2. writes your Claude login into that `ai-local-logins/<id>` folder
30
+ 3. completes it with Paperclip's own API
31
+
32
+ You get a normal **personal Paperclip AI connection**, the default for agents set to "responsible user".
33
+ - **Or paste the command** Paperclip showed you into **Paperclip showed me a terminal command → Do it for me**,
34
+ then click **Connect** in Paperclip's dialog.
35
+ - **Stays connected:** Paperclip keeps only the short-lived access token. The plugin keeps the refresh token in your
36
+ account folder, and while you have Paperclip open in a browser tab, the sidebar checks every 2 minutes. Shortly before
37
+ the token expires, it renews it with one tiny `claude -p` call (Haiku) and **reconnects the same connection**,
38
+ which rotates its secret in place. No shell, ever.
39
+
40
+ Only the owner can reconnect a personal connection, so this runs in your own browser. If you don't open Paperclip for longer than the token lives, the connection can expire. The next time you open
41
+ Paperclip it reconnects by itself within ~2 minutes. For agents that must keep running unattended, use
42
+ **Advanced → Use my Claude**, which renews entirely on the server.
43
+
44
+ ## For each user (≈30 seconds)
45
+
46
+ 1. Open **Claude accounts** in the sidebar.
47
+ 2. Click **Sign in with Claude**. A Claude page opens. Approve it and copy the code it shows.
48
+ 3. Paste the code and click **Connect**. ✅
49
+ 4. Under **Agents**, click **Use my Claude** on the agents that should run on your account.
50
+ You can also do this from the agent's **Claude login** tab.
51
+
52
+ Alternative: click **Paste a token instead** and paste the output of `claude setup-token` (run on your own computer),
53
+ or your `~/.claude/.credentials.json`.
54
+
55
+ ## How it works
56
+
57
+ - Each user gets `$PAPERCLIP_HOME/claude-accounts/<userId>/`, used as that user's `CLAUDE_CONFIG_DIR`.
58
+ - **Use my Claude** sets `CLAUDE_CONFIG_DIR` in the agent's adapter env. The browser does this through Paperclip's own API, as
59
+ the signed-in user, so normal permissions apply.
60
+ - **Sign in with Claude** runs `claude auth login` in a pseudo-terminal on the server (`script` from util-linux) and
61
+ relays the link and code through the UI.
62
+ - A keeper process watches every account. It runs on startup, every 5 min, on file changes and on agent events, plus right away
63
+ after a run fails with an auth error. For each account it:
64
+ - restores a missing, corrupt or older `.credentials.json` from the backup in Paperclip's database
65
+ - updates the backup when the CLI rotates the token (refresh tokens rotate)
66
+ - makes sure a `.claude.json` with onboarding marked done exists
67
+ - Users can only manage **their own** account. Every action is checked against the authenticated Paperclip user, and agents
68
+ and the system are rejected.
69
+ - The server's shared login (`$HOME/.claude`) is also kept alive for agents that don't use a personal account. You can turn this off.
70
+
71
+ ## Install
72
+
73
+ ```bash
74
+ npm install && npm run build
75
+ npx paperclipai plugin install ./paperclip-plugin-claude-autoauth
76
+ ```
77
+
78
+ Then in the plugin settings, set a **Backup encryption key** secret (any long random string).
79
+
80
+ ## Limits
81
+
82
+ - An agent that has a Paperclip **AI connection** selected (`runtimeConfig.aiConnection`) ignores `CLAUDE_CONFIG_DIR`.
83
+ Paperclip blanks it, and its PATCH API won't let a plugin remove the binding. The UI warns you. Switch that agent
84
+ to the local login in its settings.
85
+ - Agents with `filesystemScope` sandboxing use the server login dir instead of the per-user one.
86
+ - The browser sign-in needs `script` (util-linux; included in Debian/Ubuntu images) and `claude` on the server's PATH.
87
+ If either is missing, use **Paste a token instead**.
88
+ - A `setup-token` token can't renew itself. It is valid for about 1 year. Browser sign-in can renew forever.
89
+ - `accountsDir` must be on a persistent volume if you want sessions and settings kept. The credentials are restored either way.
90
+
91
+ ## Security
92
+
93
+ The backup holds live OAuth refresh tokens. Set the encryption key, and treat Paperclip DB backups as secrets.
94
+ Folders are created `0700` and files `0600`.
@@ -0,0 +1,95 @@
1
+ // src/manifest.ts
2
+ var manifest = {
3
+ id: "neotalk.claude-autoauth",
4
+ apiVersion: 1,
5
+ version: "0.3.0",
6
+ displayName: "Claude Accounts",
7
+ description: "Each team member connects their own Claude account from the browser (no shell), picks which agents use it, and it stays signed in across restarts.",
8
+ author: "Victor <victor@neotalk.com.br>",
9
+ categories: ["automation"],
10
+ capabilities: [
11
+ "events.subscribe",
12
+ "jobs.schedule",
13
+ "plugin.state.read",
14
+ "plugin.state.write",
15
+ "secrets.read-ref",
16
+ "agents.read",
17
+ "ui.page.register",
18
+ "ui.sidebar.register",
19
+ "ui.detailTab.register"
20
+ ],
21
+ entrypoints: {
22
+ worker: "./dist/worker.js",
23
+ ui: "./dist/ui"
24
+ },
25
+ ui: {
26
+ slots: [
27
+ { type: "page", id: "claude-accounts", displayName: "Claude accounts", exportName: "ClaudeAccountsPage", routePath: "claude-accounts" },
28
+ { type: "sidebar", id: "claude-accounts-link", displayName: "Claude accounts", exportName: "ClaudeAccountsSidebar" },
29
+ { type: "detailTab", id: "agent-claude", displayName: "Claude login", exportName: "AgentClaudeTab", entityTypes: ["agent"] }
30
+ ]
31
+ },
32
+ instanceConfigSchema: {
33
+ type: "object",
34
+ properties: {
35
+ accountsDir: {
36
+ type: "string",
37
+ title: "Where per-user Claude logins are stored",
38
+ description: "Default: $PAPERCLIP_HOME/claude-accounts. Must be on a persistent volume and readable by the agents."
39
+ },
40
+ claudeCommand: {
41
+ type: "string",
42
+ title: "Claude CLI command",
43
+ default: "claude"
44
+ },
45
+ sharedAccountEnabled: {
46
+ type: "boolean",
47
+ title: "Also keep the server's shared login alive",
48
+ description: "Backs up/restores $HOME/.claude for agents that don't use a personal account.",
49
+ default: true
50
+ },
51
+ claudeConfigDirs: {
52
+ type: "array",
53
+ items: { type: "string" },
54
+ title: "Shared login: Claude config directories",
55
+ description: "Directories that hold .credentials.json. Empty = $CLAUDE_CONFIG_DIR and $HOME/.claude. In Docker add the per-agent dirs too (e.g. /paperclip/.claude).",
56
+ default: []
57
+ },
58
+ seedCredentialsSecret: {
59
+ type: "object",
60
+ format: "secret-ref",
61
+ title: "Shared login seed: credentials.json (secret)",
62
+ description: "Optional. Paste the full contents of ~/.claude/.credentials.json from a machine where you ran `claude login`. Used only when no valid credentials exist yet (fresh container)."
63
+ },
64
+ seedOauthTokenSecret: {
65
+ type: "object",
66
+ format: "secret-ref",
67
+ title: "Shared login seed: `claude setup-token` token (secret)",
68
+ description: "Optional fallback. A 1-year sk-ant-oat01-... token. Written as a credentials file when nothing better exists."
69
+ },
70
+ encryptionKeySecret: {
71
+ type: "object",
72
+ format: "secret-ref",
73
+ title: "Backup encryption key (secret, recommended)",
74
+ description: "Optional. Any random string; encrypts the backup stored in plugin state (AES-256-GCM)."
75
+ },
76
+ secretsCompanyId: {
77
+ type: "string",
78
+ title: "Company ID owning the secrets",
79
+ description: "Needed if your Paperclip version scopes secret resolution to a company."
80
+ }
81
+ }
82
+ },
83
+ jobs: [
84
+ {
85
+ jobKey: "credentials-watchdog",
86
+ displayName: "Claude credentials watchdog",
87
+ schedule: "*/5 * * * *"
88
+ }
89
+ ]
90
+ };
91
+ var manifest_default = manifest;
92
+ export {
93
+ manifest_default as default,
94
+ manifest
95
+ };
@@ -0,0 +1,459 @@
1
+ // src/ui/index.tsx
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import {
4
+ useHostContext,
5
+ useHostNavigation,
6
+ usePluginAction,
7
+ usePluginData
8
+ } from "@paperclipai/plugin-sdk/ui";
9
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
+ async function api(method, url, body) {
11
+ const res = await fetch(`/api${url}`, {
12
+ method,
13
+ credentials: "include",
14
+ headers: body ? { "content-type": "application/json" } : void 0,
15
+ body: body ? JSON.stringify(body) : void 0
16
+ });
17
+ if (!res.ok) {
18
+ const text = await res.text().catch(() => "");
19
+ let msg = text;
20
+ try {
21
+ msg = JSON.parse(text).error ?? text;
22
+ } catch {
23
+ }
24
+ throw new Error(msg || `${method} ${url} failed (${res.status})`);
25
+ }
26
+ return res.json();
27
+ }
28
+ async function listClaudeAgents(companyId) {
29
+ const raw = await api("GET", `/companies/${companyId}/agents`);
30
+ return (Array.isArray(raw) ? raw : raw.agents ?? []).filter((a) => a.adapterType === "claude_local");
31
+ }
32
+ function envValue(v) {
33
+ if (typeof v === "string") return v;
34
+ if (v && typeof v === "object" && v.type === "plain") return String(v.value ?? "");
35
+ return null;
36
+ }
37
+ function agentConfigDir(agent) {
38
+ return envValue(agent.adapterConfig?.env?.CLAUDE_CONFIG_DIR) || null;
39
+ }
40
+ async function setAgentConfigDir(agentId, dir) {
41
+ const agent = await api("GET", `/agents/${agentId}`);
42
+ const env = { ...agent.adapterConfig?.env ?? {} };
43
+ if (dir) env.CLAUDE_CONFIG_DIR = { type: "plain", value: dir };
44
+ else delete env.CLAUDE_CONFIG_DIR;
45
+ await api("PATCH", `/agents/${agentId}`, { adapterConfig: { env } });
46
+ }
47
+ var S = {
48
+ card: { border: "1px solid var(--border, #8884)", borderRadius: 10, padding: 16, marginBottom: 16, maxWidth: 760 },
49
+ row: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" },
50
+ muted: { opacity: 0.7, fontSize: 13 },
51
+ input: { flex: 1, minWidth: 220, padding: "8px 10px", borderRadius: 6, border: "1px solid var(--border, #8886)", background: "transparent", color: "inherit", fontFamily: "monospace" },
52
+ btn: { padding: "7px 12px", borderRadius: 6, border: "1px solid var(--border, #8886)", background: "transparent", color: "inherit", cursor: "pointer" },
53
+ primary: { padding: "7px 14px", borderRadius: 6, border: "none", background: "#d97757", color: "#fff", cursor: "pointer", fontWeight: 600 },
54
+ ok: { color: "#2e9d5b", fontWeight: 600 },
55
+ bad: { color: "#d14343", fontWeight: 600 },
56
+ code: { fontFamily: "monospace", fontSize: 12, wordBreak: "break-all" }
57
+ };
58
+ function Btn(p) {
59
+ return /* @__PURE__ */ jsx("button", { style: { ...p.primary ? S.primary : S.btn, opacity: p.disabled ? 0.5 : 1 }, disabled: p.disabled, onClick: p.onClick, children: p.children });
60
+ }
61
+ function useMyAccount() {
62
+ const me = usePluginAction("me");
63
+ const [account, setAccount] = useState(null);
64
+ const [error, setError] = useState(null);
65
+ const refresh = useCallback(async () => {
66
+ try {
67
+ setAccount(await me());
68
+ setError(null);
69
+ } catch (e) {
70
+ setError(String(e.message ?? e));
71
+ }
72
+ }, [me]);
73
+ useEffect(() => {
74
+ void refresh();
75
+ }, [refresh]);
76
+ return { account, setAccount, error, refresh };
77
+ }
78
+ function ConnectCard({ account, onChange }) {
79
+ const loginStart = usePluginAction("login-start");
80
+ const loginCode = usePluginAction("login-code");
81
+ const loginCancel = usePluginAction("login-cancel");
82
+ const saveToken = usePluginAction("save-token");
83
+ const disconnect = usePluginAction("disconnect");
84
+ const [url, setUrl] = useState(account?.login?.url ?? null);
85
+ const [code, setCode] = useState("");
86
+ const [paste, setPaste] = useState("");
87
+ const [showPaste, setShowPaste] = useState(false);
88
+ const [busy, setBusy] = useState(false);
89
+ const [msg, setMsg] = useState(null);
90
+ const run = async (fn) => {
91
+ setBusy(true);
92
+ setMsg(null);
93
+ try {
94
+ await fn();
95
+ } catch (e) {
96
+ setMsg({ ok: false, text: String(e.message ?? e) });
97
+ } finally {
98
+ setBusy(false);
99
+ }
100
+ };
101
+ if (!account) return /* @__PURE__ */ jsx("div", { style: S.card, children: "Loading\u2026" });
102
+ return /* @__PURE__ */ jsxs("div", { style: S.card, children: [
103
+ /* @__PURE__ */ jsx("h3", { style: { marginTop: 0 }, children: "My Claude account" }),
104
+ account.connected ? /* @__PURE__ */ jsxs(Fragment, { children: [
105
+ /* @__PURE__ */ jsxs("div", { style: S.row, children: [
106
+ /* @__PURE__ */ jsx("span", { style: S.ok, children: "\u25CF Connected" }),
107
+ /* @__PURE__ */ jsx("span", { style: S.muted, children: account.hasRefreshToken ? "renews automatically" : `token valid until ${account.expiresAt?.slice(0, 10) ?? "?"}` })
108
+ ] }),
109
+ /* @__PURE__ */ jsx("div", { style: { ...S.row, marginTop: 12 }, children: /* @__PURE__ */ jsx(
110
+ Btn,
111
+ {
112
+ disabled: busy,
113
+ onClick: () => run(async () => {
114
+ if (!confirm("Disconnect your Claude account? Agents using it will stop working.")) return;
115
+ await disconnect();
116
+ onChange(null);
117
+ }),
118
+ children: "Disconnect"
119
+ }
120
+ ) })
121
+ ] }) : url ? /* @__PURE__ */ jsxs(Fragment, { children: [
122
+ /* @__PURE__ */ jsxs("p", { style: { marginTop: 0 }, children: [
123
+ /* @__PURE__ */ jsx("b", { children: "1." }),
124
+ " ",
125
+ /* @__PURE__ */ jsx("a", { href: url, target: "_blank", rel: "noreferrer", children: "Open the Claude sign-in page \u2197" }),
126
+ " ",
127
+ "and approve."
128
+ ] }),
129
+ /* @__PURE__ */ jsxs("p", { children: [
130
+ /* @__PURE__ */ jsx("b", { children: "2." }),
131
+ " Paste the code that Claude shows you:"
132
+ ] }),
133
+ /* @__PURE__ */ jsxs("div", { style: S.row, children: [
134
+ /* @__PURE__ */ jsx("input", { style: S.input, value: code, onChange: (e) => setCode(e.target.value), placeholder: "code#state", autoFocus: true }),
135
+ /* @__PURE__ */ jsx(
136
+ Btn,
137
+ {
138
+ primary: true,
139
+ disabled: busy || !code.trim(),
140
+ onClick: () => run(async () => {
141
+ const r = await loginCode({ code });
142
+ setMsg({ ok: r.ok, text: r.message });
143
+ if (r.ok) {
144
+ setUrl(null);
145
+ onChange(r.status);
146
+ }
147
+ }),
148
+ children: busy ? "Connecting\u2026" : "Connect"
149
+ }
150
+ ),
151
+ /* @__PURE__ */ jsx(
152
+ Btn,
153
+ {
154
+ disabled: busy,
155
+ onClick: () => run(async () => {
156
+ await loginCancel();
157
+ setUrl(null);
158
+ }),
159
+ children: "Cancel"
160
+ }
161
+ )
162
+ ] })
163
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
164
+ /* @__PURE__ */ jsx("p", { style: { marginTop: 0 }, children: "Connect your own Claude Pro/Max account. Your agents will use it, and it stays signed in." }),
165
+ /* @__PURE__ */ jsxs("div", { style: S.row, children: [
166
+ /* @__PURE__ */ jsx(
167
+ Btn,
168
+ {
169
+ primary: true,
170
+ disabled: busy,
171
+ onClick: () => run(async () => {
172
+ const r = await loginStart();
173
+ setUrl(r.url);
174
+ window.open(r.url, "_blank", "noopener");
175
+ }),
176
+ children: busy ? "Starting\u2026" : "Sign in with Claude"
177
+ }
178
+ ),
179
+ /* @__PURE__ */ jsx(Btn, { onClick: () => setShowPaste((v) => !v), children: "Paste a token instead" })
180
+ ] }),
181
+ showPaste && /* @__PURE__ */ jsxs("div", { style: { marginTop: 12 }, children: [
182
+ /* @__PURE__ */ jsxs("p", { style: S.muted, children: [
183
+ "On your computer run ",
184
+ /* @__PURE__ */ jsx("code", { children: "claude setup-token" }),
185
+ " and paste the ",
186
+ /* @__PURE__ */ jsx("code", { children: "sk-ant-oat\u2026" }),
187
+ " token, or paste the contents of ",
188
+ /* @__PURE__ */ jsx("code", { children: "~/.claude/.credentials.json" }),
189
+ "."
190
+ ] }),
191
+ /* @__PURE__ */ jsxs("div", { style: S.row, children: [
192
+ /* @__PURE__ */ jsx("input", { style: S.input, type: "password", value: paste, onChange: (e) => setPaste(e.target.value), placeholder: "sk-ant-oat01-\u2026" }),
193
+ /* @__PURE__ */ jsx(
194
+ Btn,
195
+ {
196
+ primary: true,
197
+ disabled: busy || !paste.trim(),
198
+ onClick: () => run(async () => {
199
+ onChange(await saveToken({ value: paste }));
200
+ setPaste("");
201
+ setShowPaste(false);
202
+ }),
203
+ children: "Save"
204
+ }
205
+ )
206
+ ] })
207
+ ] })
208
+ ] }),
209
+ msg && /* @__PURE__ */ jsx("p", { style: msg.ok ? S.ok : S.bad, children: msg.text })
210
+ ] });
211
+ }
212
+ function AgentToggle({ agent, account, onDone }) {
213
+ const [busy, setBusy] = useState(false);
214
+ const [err, setErr] = useState(null);
215
+ const dir = agentConfigDir(agent);
216
+ const mine = dir === account.claudeConfigDir;
217
+ const bound = !!agent.runtimeConfig?.aiConnection;
218
+ const set = async (value) => {
219
+ setBusy(true);
220
+ setErr(null);
221
+ try {
222
+ await setAgentConfigDir(agent.id, value);
223
+ onDone();
224
+ } catch (e) {
225
+ setErr(String(e.message ?? e));
226
+ } finally {
227
+ setBusy(false);
228
+ }
229
+ };
230
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
231
+ mine ? /* @__PURE__ */ jsx(Btn, { disabled: busy, onClick: () => set(null), children: "Stop using my Claude" }) : /* @__PURE__ */ jsx(Btn, { primary: true, disabled: busy || !account.connected, onClick: () => set(account.claudeConfigDir), children: "Use my Claude" }),
232
+ bound && /* @__PURE__ */ jsx("span", { style: { ...S.muted, color: "#c48a00" }, children: "\u26A0 This agent has a Paperclip \u201CAI connection\u201D selected, which overrides the folder. Switch it to the local login in the agent's settings." }),
233
+ err && /* @__PURE__ */ jsx("span", { style: S.bad, children: err })
234
+ ] });
235
+ }
236
+ function AgentsCard({ companyId, account }) {
237
+ const [agents, setAgents] = useState(null);
238
+ const [err, setErr] = useState(null);
239
+ const load = useCallback(() => {
240
+ listClaudeAgents(companyId).then(setAgents).catch((e) => setErr(String(e.message ?? e)));
241
+ }, [companyId]);
242
+ useEffect(load, [load]);
243
+ return /* @__PURE__ */ jsxs("div", { style: S.card, children: [
244
+ /* @__PURE__ */ jsx("h3", { style: { marginTop: 0 }, children: "Agents" }),
245
+ /* @__PURE__ */ jsx("p", { style: S.muted, children: "Choose which Claude agents run on your account. Each person can connect their own." }),
246
+ err && /* @__PURE__ */ jsx("p", { style: S.bad, children: err }),
247
+ !agents ? /* @__PURE__ */ jsx("p", { children: "Loading\u2026" }) : agents.length === 0 ? /* @__PURE__ */ jsx("p", { style: S.muted, children: "No Claude (claude_local) agents in this company." }) : agents.map((a) => {
248
+ const dir = agentConfigDir(a);
249
+ return /* @__PURE__ */ jsxs("div", { style: { ...S.row, padding: "8px 0", borderTop: "1px solid var(--border, #8882)" }, children: [
250
+ /* @__PURE__ */ jsx("span", { style: { minWidth: 160, fontWeight: 600 }, children: a.name }),
251
+ /* @__PURE__ */ jsx("span", { style: { ...S.muted, minWidth: 130 }, children: dir === account.claudeConfigDir ? "uses your Claude" : dir ? "uses another account" : "server login" }),
252
+ /* @__PURE__ */ jsx(AgentToggle, { agent: a, account, onDone: load })
253
+ ] }, a.id);
254
+ })
255
+ ] });
256
+ }
257
+ async function myClaudeConnections(companyId) {
258
+ const r = await api("GET", `/companies/${companyId}/ai-connections`);
259
+ return r.connections.filter(
260
+ (c) => c.provider === "anthropic" && c.method === "subscription" && c.ownership === "personal" && c.ownerUserId === r.currentUserId
261
+ );
262
+ }
263
+ async function connectToPaperclip(companyId, fill, record, opts = {}) {
264
+ const intent = {
265
+ provider: "anthropic",
266
+ method: "subscription",
267
+ name: opts.name ?? "My Claude",
268
+ ownership: "personal",
269
+ agentIds: [],
270
+ allAgents: false,
271
+ ...opts.connectionId ? { connectionId: opts.connectionId } : {}
272
+ };
273
+ const attempt = await api("POST", `/companies/${companyId}/ai-connections/local/attempts`, {
274
+ ...intent,
275
+ restart: true
276
+ });
277
+ const filled = await fill({ command: attempt.command });
278
+ if (!filled.filled) {
279
+ await api("DELETE", `/companies/${companyId}/ai-connections/local/attempts/${attempt.sessionId}`).catch(() => {
280
+ });
281
+ return { connectionId: "", needsSignIn: true };
282
+ }
283
+ const saved = await api("POST", `/companies/${companyId}/ai-connections/local`, {
284
+ ...intent,
285
+ localSessionId: attempt.sessionId
286
+ });
287
+ await record({ companyId, connectionId: saved.connectionId, name: intent.name });
288
+ return { connectionId: saved.connectionId };
289
+ }
290
+ function PaperclipConnectionCard({ companyId, account }) {
291
+ const fill = usePluginAction("native-fill");
292
+ const record = usePluginAction("native-record");
293
+ const [conns, setConns] = useState(null);
294
+ const [cmd, setCmd] = useState("");
295
+ const [busy, setBusy] = useState(false);
296
+ const [msg, setMsg] = useState(null);
297
+ const load = useCallback(() => {
298
+ myClaudeConnections(companyId).then(setConns).catch(() => setConns([]));
299
+ }, [companyId]);
300
+ useEffect(load, [load]);
301
+ const run = async (fn) => {
302
+ setBusy(true);
303
+ setMsg(null);
304
+ try {
305
+ setMsg({ ok: true, text: await fn() });
306
+ load();
307
+ } catch (e) {
308
+ setMsg({ ok: false, text: String(e.message ?? e) });
309
+ } finally {
310
+ setBusy(false);
311
+ }
312
+ };
313
+ const existing = conns?.find((c) => c.isDefault) ?? conns?.[0];
314
+ return /* @__PURE__ */ jsxs("div", { style: S.card, children: [
315
+ /* @__PURE__ */ jsx("h3", { style: { marginTop: 0 }, children: "Paperclip AI connection" }),
316
+ /* @__PURE__ */ jsx("p", { style: S.muted, children: "Saves your Claude as your personal Paperclip AI connection, with no terminal. Agents set to \u201Cresponsible user\u201D use it. The plugin renews it before it expires." }),
317
+ existing && /* @__PURE__ */ jsxs("p", { children: [
318
+ /* @__PURE__ */ jsxs("span", { style: existing.status === "connected" ? S.ok : S.bad, children: [
319
+ "\u25CF ",
320
+ existing.name
321
+ ] }),
322
+ " ",
323
+ /* @__PURE__ */ jsxs("span", { style: S.muted, children: [
324
+ existing.status,
325
+ existing.isDefault ? " \xB7 default" : ""
326
+ ] })
327
+ ] }),
328
+ /* @__PURE__ */ jsxs("div", { style: S.row, children: [
329
+ /* @__PURE__ */ jsx(
330
+ Btn,
331
+ {
332
+ primary: true,
333
+ disabled: busy || !account.connected,
334
+ onClick: () => run(async () => {
335
+ const r = await connectToPaperclip(companyId, fill, record, existing ? { connectionId: existing.id, name: existing.name } : {});
336
+ return r.needsSignIn ? "Sign in with Claude above first." : existing ? "Reconnected \u2713" : "Connected \u2713";
337
+ }),
338
+ children: busy ? "Connecting\u2026" : existing ? "Reconnect now" : "Connect to Paperclip"
339
+ }
340
+ ),
341
+ !account.connected && /* @__PURE__ */ jsx("span", { style: S.muted, children: "Sign in with Claude above first." })
342
+ ] }),
343
+ /* @__PURE__ */ jsxs("details", { style: { marginTop: 14 }, children: [
344
+ /* @__PURE__ */ jsx("summary", { style: { cursor: "pointer" }, children: "Paperclip showed me a terminal command" }),
345
+ /* @__PURE__ */ jsxs("p", { style: S.muted, children: [
346
+ "Paste it here. It looks like ",
347
+ /* @__PURE__ */ jsx("code", { style: S.code, children: "(export CLAUDE_CONFIG_DIR='\u2026/ai-local-logins/\u2026' && \u2026 claude auth login)" }),
348
+ ". The plugin does that step for you. Then click ",
349
+ /* @__PURE__ */ jsx("b", { children: "Connect" }),
350
+ " in Paperclip's dialog."
351
+ ] }),
352
+ /* @__PURE__ */ jsxs("div", { style: S.row, children: [
353
+ /* @__PURE__ */ jsx("input", { style: S.input, value: cmd, onChange: (e) => setCmd(e.target.value), placeholder: "(export CLAUDE_CONFIG_DIR=\u2026" }),
354
+ /* @__PURE__ */ jsx(
355
+ Btn,
356
+ {
357
+ disabled: busy || !cmd.trim(),
358
+ onClick: () => run(async () => {
359
+ const r = await fill({ command: cmd });
360
+ if (!r.filled) throw new Error("Sign in with Claude above first, then paste again.");
361
+ setCmd("");
362
+ return "Done. Now click Connect in Paperclip's dialog.";
363
+ }),
364
+ children: "Do it for me"
365
+ }
366
+ )
367
+ ] })
368
+ ] }),
369
+ msg && /* @__PURE__ */ jsx("p", { style: msg.ok ? S.ok : S.bad, children: msg.text })
370
+ ] });
371
+ }
372
+ function useNativeKeepAlive(companyId) {
373
+ const keepalive = usePluginAction("native-keepalive");
374
+ const fill = usePluginAction("native-fill");
375
+ const record = usePluginAction("native-record");
376
+ const busy = useRef(false);
377
+ useEffect(() => {
378
+ if (!companyId) return;
379
+ const tick = async () => {
380
+ if (busy.current) return;
381
+ busy.current = true;
382
+ try {
383
+ const r = await keepalive();
384
+ if (r.action === "discover") {
385
+ const mine = await myClaudeConnections(companyId);
386
+ const pick = mine.find((c) => c.isDefault) ?? mine[0];
387
+ if (pick) await record({ companyId, connectionId: pick.id, name: pick.name });
388
+ } else if (r.action === "push" && r.companyId && r.connectionId) {
389
+ await connectToPaperclip(r.companyId, fill, record, { connectionId: r.connectionId, name: r.name });
390
+ }
391
+ } catch {
392
+ } finally {
393
+ busy.current = false;
394
+ }
395
+ };
396
+ void tick();
397
+ const t = setInterval(tick, 2 * 60 * 1e3);
398
+ return () => clearInterval(t);
399
+ }, [companyId, keepalive, fill, record]);
400
+ }
401
+ function ClaudeAccountsPage(_props) {
402
+ const { companyId } = useHostContext();
403
+ const { account, setAccount, error, refresh } = useMyAccount();
404
+ return /* @__PURE__ */ jsxs("div", { style: { padding: 24 }, children: [
405
+ /* @__PURE__ */ jsx("h2", { style: { marginTop: 0 }, children: "Claude accounts" }),
406
+ error && /* @__PURE__ */ jsx("p", { style: S.bad, children: error }),
407
+ /* @__PURE__ */ jsx(ConnectCard, { account, onChange: (a) => a ? setAccount(a) : void refresh() }),
408
+ account && companyId && /* @__PURE__ */ jsx(PaperclipConnectionCard, { companyId, account }),
409
+ account && companyId && /* @__PURE__ */ jsxs("details", { children: [
410
+ /* @__PURE__ */ jsx("summary", { style: { cursor: "pointer", marginBottom: 12 }, children: "Advanced: run specific agents directly on my login folder" }),
411
+ /* @__PURE__ */ jsx(AgentsCard, { companyId, account })
412
+ ] })
413
+ ] });
414
+ }
415
+ function ClaudeAccountsSidebar(_props) {
416
+ const nav = useHostNavigation();
417
+ const { companyId } = useHostContext();
418
+ useNativeKeepAlive(companyId);
419
+ return /* @__PURE__ */ jsx("a", { ...nav.linkProps("/claude-accounts"), children: "Claude accounts" });
420
+ }
421
+ function AgentClaudeTab({ context }) {
422
+ const { account, setAccount, refresh } = useMyAccount();
423
+ const [agent, setAgent] = useState(null);
424
+ const [err, setErr] = useState(null);
425
+ const load = useCallback(() => {
426
+ api("GET", `/agents/${context.entityId}`).then(setAgent).catch((e) => setErr(String(e.message ?? e)));
427
+ }, [context.entityId]);
428
+ useEffect(load, [load]);
429
+ const dir = agent ? agentConfigDir(agent) : null;
430
+ const owner = usePluginData("dir-owner", { dir: dir ?? "" });
431
+ if (err) return /* @__PURE__ */ jsx("p", { style: S.bad, children: err });
432
+ if (!agent || !account) return /* @__PURE__ */ jsx("p", { children: "Loading\u2026" });
433
+ if (agent.adapterType !== "claude_local") return /* @__PURE__ */ jsx("p", { style: S.muted, children: "This agent does not use Claude Code." });
434
+ const who = !dir ? "the server's shared login" : dir === account.claudeConfigDir ? "your Claude account" : owner.data?.managed ? "another team member's Claude account" : `a custom folder (${dir})`;
435
+ return /* @__PURE__ */ jsxs("div", { style: { padding: 16 }, children: [
436
+ /* @__PURE__ */ jsxs("p", { children: [
437
+ "This agent signs in with ",
438
+ /* @__PURE__ */ jsx("b", { children: who }),
439
+ "."
440
+ ] }),
441
+ !account.connected && /* @__PURE__ */ jsx(ConnectCard, { account, onChange: (a) => a ? setAccount(a) : void refresh() }),
442
+ /* @__PURE__ */ jsx("div", { style: S.row, children: /* @__PURE__ */ jsx(
443
+ AgentToggle,
444
+ {
445
+ agent,
446
+ account,
447
+ onDone: () => {
448
+ load();
449
+ owner.refresh();
450
+ }
451
+ }
452
+ ) })
453
+ ] });
454
+ }
455
+ export {
456
+ AgentClaudeTab,
457
+ ClaudeAccountsPage,
458
+ ClaudeAccountsSidebar
459
+ };