shraga 0.1.126 → 0.1.127
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/dist/client/assets/{index-sDoAjV2H.js → index-DRTvNKXl.js} +180 -180
- package/dist/client/index.html +1 -1
- package/package.json +1 -1
- package/src/client/components/ClaudeAccountSection.tsx +31 -12
- package/src/server/boot.ts +8 -1
- package/src/server/claude-account.ts +8 -2
- package/src/server/claude-login.ts +14 -3
package/dist/client/index.html
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
14
14
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
15
15
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
|
|
16
|
-
<script type="module" crossorigin src="/assets/index-
|
|
16
|
+
<script type="module" crossorigin src="/assets/index-DRTvNKXl.js"></script>
|
|
17
17
|
<link rel="stylesheet" crossorigin href="/assets/index-_XEpjgJO.css">
|
|
18
18
|
</head>
|
|
19
19
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shraga",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.127",
|
|
4
4
|
"description": "The teammate you delegate coding to — a self-hostable, multi-user AI coding agent web UI (Claude Code, with a pluggable engine seam).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -2,9 +2,10 @@ import { useCallback, useEffect, useState } from 'react';
|
|
|
2
2
|
import { Button } from './ui/button';
|
|
3
3
|
import { Input } from './ui/input';
|
|
4
4
|
import { api } from '@/lib/api';
|
|
5
|
+
import { UsageMetric, type Usage } from './MachineStats';
|
|
5
6
|
|
|
6
|
-
interface AccountStatus { connected: boolean; account: string | null; subscriptionType: string | null }
|
|
7
|
-
interface Accounts { me: AccountStatus | null; global?: AccountStatus }
|
|
7
|
+
interface AccountStatus { connected: boolean; account: string | null; subscriptionType: string | null; useShared?: boolean }
|
|
8
|
+
interface Accounts { me: AccountStatus | null; global?: AccountStatus; usage?: { me: Usage | null; global: Usage | null } }
|
|
8
9
|
type Target = 'me' | 'global';
|
|
9
10
|
|
|
10
11
|
/** Connect / replace / disconnect a Claude Code subscription login from the browser (no SSH). */
|
|
@@ -44,21 +45,38 @@ export function ClaudeAccountSection({ getToken }: { getToken: () => Promise<str
|
|
|
44
45
|
await api(`/api/claude-account/${target}`, getToken, { method: 'DELETE' });
|
|
45
46
|
refresh();
|
|
46
47
|
});
|
|
48
|
+
const useShared = (on: boolean) => act(async () => {
|
|
49
|
+
await api('/api/claude-account/me/use-shared', getToken, { method: 'PUT', body: JSON.stringify({ on }) });
|
|
50
|
+
refresh();
|
|
51
|
+
});
|
|
47
52
|
|
|
48
|
-
const
|
|
53
|
+
const active = (target: Target) => target === 'global'
|
|
54
|
+
? !accounts?.me?.connected || !!accounts.me.useShared
|
|
55
|
+
: !!accounts?.me?.connected && !accounts.me.useShared;
|
|
56
|
+
|
|
57
|
+
const row = (target: Target, title: string, s: AccountStatus | null, canManage = true) => (
|
|
49
58
|
<div className="flex items-center justify-between gap-2 flex-wrap">
|
|
50
59
|
<div className="text-sm min-w-0">
|
|
51
|
-
<div className="font-medium">
|
|
60
|
+
<div className="font-medium flex items-center gap-2">
|
|
61
|
+
{title}
|
|
62
|
+
{active(target) && <span className="text-[10px] uppercase tracking-wide text-emerald-500">active</span>}
|
|
63
|
+
<UsageMetric usage={accounts?.usage?.[target] ?? null} />
|
|
64
|
+
</div>
|
|
52
65
|
<div className="text-xs text-muted-foreground truncate">
|
|
53
|
-
{s?.connected ? `${s.account ?? 'Connected'}${s.subscriptionType ? ` · ${s.subscriptionType}` : ''}` : target === 'me' ? 'Not connected — using the shared subscription' : 'Not connected'}
|
|
66
|
+
{s?.connected ? `${s.account ?? 'Connected'}${s.subscriptionType ? ` · ${s.subscriptionType}` : ''}` : target === 'me' ? 'Not connected — using the shared subscription' : canManage ? 'Not connected' : ''}
|
|
54
67
|
</div>
|
|
55
68
|
</div>
|
|
56
|
-
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
{canManage && (
|
|
70
|
+
<div className="flex gap-2">
|
|
71
|
+
<Button size="sm" variant="outline" disabled={busy} onClick={() => connect(target)}>{s?.connected ? 'Replace' : 'Connect'}</Button>
|
|
72
|
+
{target === 'me' && s?.connected && (
|
|
73
|
+
<>
|
|
74
|
+
<Button size="sm" variant="ghost" disabled={busy} onClick={() => useShared(!s.useShared)}>{s.useShared ? 'Use mine' : 'Use shared'}</Button>
|
|
75
|
+
<Button size="sm" variant="ghost" disabled={busy} onClick={() => disconnect(target)}>Sign out</Button>
|
|
76
|
+
</>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
62
80
|
</div>
|
|
63
81
|
);
|
|
64
82
|
|
|
@@ -69,7 +87,8 @@ export function ClaudeAccountSection({ getToken }: { getToken: () => Promise<str
|
|
|
69
87
|
{accounts.me ? row('me', 'Your subscription', accounts.me) : (
|
|
70
88
|
<p className="text-xs text-muted-foreground">Personal subscription unavailable: you are not a contact on this agent.</p>
|
|
71
89
|
)}
|
|
72
|
-
{accounts.global
|
|
90
|
+
{accounts.global ? row('global', 'Shared subscription', accounts.global)
|
|
91
|
+
: accounts.usage?.global && row('global', 'Shared subscription', null, false)}
|
|
73
92
|
{pending && (
|
|
74
93
|
<div className="rounded-md bg-muted/50 px-3 py-2 space-y-2 text-xs">
|
|
75
94
|
<p>
|
package/src/server/boot.ts
CHANGED
|
@@ -295,7 +295,14 @@ app.get('/api/claude-account', requireAuth, claudeLoginRoute(async (_req, res, u
|
|
|
295
295
|
return null;
|
|
296
296
|
});
|
|
297
297
|
const global = user?.isOwner ? await claudeLogin.status(claudeLogin.resolve('global', user)) : undefined;
|
|
298
|
-
|
|
298
|
+
// Cached readings only (the reader dedupes + TTLs upstream) — shared usage is visible to every member.
|
|
299
|
+
const meDir = me?.connected ? claudeLogin.resolve('me', user).dir : null;
|
|
300
|
+
const [meUsage, globalUsage] = await Promise.all([meDir ? claudeUsageFor(meDir).get() : null, claudeUsageFor(null).get()]);
|
|
301
|
+
res.json({ me, global, usage: { me: meUsage, global: globalUsage } });
|
|
302
|
+
}));
|
|
303
|
+
app.put('/api/claude-account/:target/use-shared', requireAuth, claudeLoginRoute(async (req, res, user) => {
|
|
304
|
+
await claudeLogin.setUseShared(claudeLogin.resolve(String(req.params.target), user), !!req.body?.on);
|
|
305
|
+
res.json({ ok: true });
|
|
299
306
|
}));
|
|
300
307
|
app.post('/api/claude-account/:target/login', requireAuth, claudeLoginRoute(async (req, res, user) => {
|
|
301
308
|
res.json({ url: await claudeLogin.start(claudeLogin.resolve(String(req.params.target), user)) });
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `CLAUDE_CONFIG_DIR=<dir> claude auth login`). Anything else → null → the default account, unchanged.
|
|
5
5
|
* The folder is data-sync ignored (see DataSync.GITIGNORE_ENTRIES) and hidden from the workspace UI (dotdir).
|
|
6
6
|
*/
|
|
7
|
-
import { statSync } from 'node:fs';
|
|
7
|
+
import { existsSync, statSync } from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import * as contacts from './contacts.ts';
|
|
10
10
|
import { WORKSPACE_DIR } from './workspace.ts';
|
|
@@ -19,7 +19,13 @@ export function claudeAccountDir(
|
|
|
19
19
|
const contact = email ? find(email) : null;
|
|
20
20
|
if (!contact?.id) return null;
|
|
21
21
|
const dir = path.join(workspaceDir, 'users', contact.id, '.claude');
|
|
22
|
-
try { return statSync(dir).isDirectory() ? dir : null; } catch { return null; }
|
|
22
|
+
try { return statSync(dir).isDirectory() && !usesShared(dir) ? dir : null; } catch { return null; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Marker inside a personal login dir: keep the credentials but route runs to the shared login (hot-switch). */
|
|
26
|
+
export const USE_SHARED_MARKER = '.shraga-use-shared';
|
|
27
|
+
export function usesShared(dir: string): boolean {
|
|
28
|
+
return existsSync(path.join(dir, USE_SHARED_MARKER));
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
/** The login a run on `dir` (null = the box default) is signed in as, for provenance. Never a token. */
|
|
@@ -14,7 +14,7 @@ import { createRequire } from 'node:module';
|
|
|
14
14
|
import { homedir, tmpdir } from 'node:os';
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import * as contacts from './contacts.ts';
|
|
17
|
-
import { applyClaudeAccount } from './claude-account.ts';
|
|
17
|
+
import { applyClaudeAccount, usesShared, USE_SHARED_MARKER } from './claude-account.ts';
|
|
18
18
|
import { WORKSPACE_DIR } from './workspace.ts';
|
|
19
19
|
|
|
20
20
|
const TAG = '[claude-login]';
|
|
@@ -32,7 +32,7 @@ export interface ClaudeTarget {
|
|
|
32
32
|
explicitDir: boolean;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export interface ClaudeAccountStatus { connected: boolean; account: string | null; subscriptionType: string | null }
|
|
35
|
+
export interface ClaudeAccountStatus { connected: boolean; account: string | null; subscriptionType: string | null; useShared?: boolean }
|
|
36
36
|
|
|
37
37
|
export class ClaudeLoginError extends Error {
|
|
38
38
|
constructor(public status: number, message: string) { super(message); }
|
|
@@ -85,7 +85,8 @@ export class ClaudeLogin {
|
|
|
85
85
|
try {
|
|
86
86
|
const out = await this.run(['auth', 'status', '--json'], target.explicitDir ? target.dir : null);
|
|
87
87
|
const s = JSON.parse(out);
|
|
88
|
-
|
|
88
|
+
const useShared = target.name === 'me' && usesShared(target.dir);
|
|
89
|
+
return { connected: !!s.loggedIn, account: s.email ?? null, subscriptionType: s.subscriptionType ?? null, ...(useShared && { useShared }) };
|
|
89
90
|
} catch (err) {
|
|
90
91
|
console.warn(`${TAG} status failed for ${target.key}:`, (err as Error).message);
|
|
91
92
|
return none;
|
|
@@ -145,10 +146,20 @@ export class ClaudeLogin {
|
|
|
145
146
|
} finally {
|
|
146
147
|
if (this.options.swap) await rm(p.loginDir, { recursive: true, force: true });
|
|
147
148
|
}
|
|
149
|
+
if (target.name === 'me') await rm(path.join(target.dir, USE_SHARED_MARKER), { force: true }); // (re)connecting = use it
|
|
148
150
|
this.options.onChange(target);
|
|
149
151
|
return this.status(target);
|
|
150
152
|
}
|
|
151
153
|
|
|
154
|
+
/** `me` only: hot-switch between the personal login and the shared one, keeping the credentials. */
|
|
155
|
+
async setUseShared(target: ClaudeTarget, on: boolean): Promise<void> {
|
|
156
|
+
if (target.name !== 'me') throw new ClaudeLoginError(400, 'Only a personal login can be switched');
|
|
157
|
+
if (!(await exists(target.dir))) throw new ClaudeLoginError(409, 'No personal login to switch');
|
|
158
|
+
const marker = path.join(target.dir, USE_SHARED_MARKER);
|
|
159
|
+
// No onChange: the login itself is unchanged, and each dir's usage cache stays valid.
|
|
160
|
+
if (on) await writeFile(marker, ''); else await rm(marker, { force: true });
|
|
161
|
+
}
|
|
162
|
+
|
|
152
163
|
/** `me`: logout + remove the dir → runs fall back to the shared subscription. `global`: logout. */
|
|
153
164
|
async disconnect(target: ClaudeTarget): Promise<void> {
|
|
154
165
|
this.cancel(target.key);
|