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 +94 -0
- package/dist/manifest.js +95 -0
- package/dist/ui/index.js +459 -0
- package/dist/worker.js +37268 -0
- package/package.json +35 -0
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`.
|
package/dist/manifest.js
ADDED
|
@@ -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
|
+
};
|
package/dist/ui/index.js
ADDED
|
@@ -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
|
+
};
|