gigarag-cursor 0.1.1 → 0.2.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gigarag",
3
3
  "description": "Your GigaRAG knowledge base inside Cursor: index a codebase, save decisions, recall them later.",
4
- "version": "0.1.1",
4
+ "version": "0.2.1",
5
5
  "author": { "name": "GigaRAG", "url": "https://gigarag.com" },
6
6
  "homepage": "https://gigarag.com/connect/cursor",
7
7
  "license": "UNLICENSED",
@@ -0,0 +1,106 @@
1
+ import { updateConfig } from '../config.js';
2
+ import { DEFAULT_MCP_URL } from '../constants.js';
3
+ import { assertSecureUrl } from '../secureUrl.js';
4
+ import { ACCOUNT_SLOT, configuredIssuer, loadAccountCredential, refreshOAuth, workspaceSlot, writeSlot, currentGeneration, } from './credentials.js';
5
+ /**
6
+ * The account sign-in: list every workspace the person can reach, and trade
7
+ * the account token for one workspace's token.
8
+ *
9
+ * The account token opens no workspace by itself. The app refuses it at /mcp
10
+ * and says so, so everything that talks to a workspace goes through
11
+ * `exchangeForWorkspace` first. See section 9 of the app's sharing levels spec.
12
+ */
13
+ export const TOKEN_EXCHANGE = 'urn:ietf:params:oauth:grant-type:token-exchange';
14
+ const ACCESS_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
15
+ const REFRESH_MARGIN_MS = 60_000;
16
+ export class AccountError extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = 'AccountError';
20
+ }
21
+ }
22
+ const NOT_SIGNED_IN = 'This computer has no GigaRAG account sign-in. Run: gigarag login';
23
+ /** The account credential, refreshed when it is about to lapse. */
24
+ async function accountCredential(fetchImpl, now, force = false) {
25
+ const credential = loadAccountCredential();
26
+ if (!credential)
27
+ throw new AccountError(NOT_SIGNED_IN);
28
+ const due = credential.expiresAt !== undefined && credential.expiresAt - now() < REFRESH_MARGIN_MS;
29
+ if ((force || due) && credential.refreshToken) {
30
+ try {
31
+ return await refreshOAuth(credential, fetchImpl, now, ACCOUNT_SLOT);
32
+ }
33
+ catch (err) {
34
+ throw new AccountError(err.message);
35
+ }
36
+ }
37
+ return credential;
38
+ }
39
+ export async function listAccountWorkspaces(opts = {}) {
40
+ const fetchImpl = opts.fetch ?? fetch;
41
+ const now = opts.now ?? Date.now;
42
+ const issuer = (opts.issuer ?? configuredIssuer()).replace(/\/$/, '');
43
+ const url = assertSecureUrl(`${issuer}/api/cli/workspaces`, 'The GigaRAG app');
44
+ const call = async (credential) => fetchImpl(url, {
45
+ headers: { authorization: `Bearer ${credential.accessToken}`, accept: 'application/json' },
46
+ signal: AbortSignal.timeout(30_000),
47
+ });
48
+ let res = await call(await accountCredential(fetchImpl, now));
49
+ if (res.status === 401)
50
+ res = await call(await accountCredential(fetchImpl, now, true));
51
+ const body = (await res.json().catch(() => ({})));
52
+ if (!res.ok || !Array.isArray(body.workspaces)) {
53
+ throw new AccountError(body.error ?? `Could not list your workspaces (HTTP ${res.status}). Run: gigarag login`);
54
+ }
55
+ updateConfig(c => {
56
+ c.workspaceNames = Object.fromEntries(body.workspaces.map(w => [w.id, w.name]));
57
+ });
58
+ return body.workspaces;
59
+ }
60
+ export async function exchangeForWorkspace(workspaceId, opts = {}) {
61
+ const fetchImpl = opts.fetch ?? fetch;
62
+ const now = opts.now ?? Date.now;
63
+ // Read before the account credential: a browser sign-in that lands while this exchange is in
64
+ // flight moves the generation on, and the old account's token must not be stored under the new one.
65
+ const generation = currentGeneration();
66
+ const account = await accountCredential(fetchImpl, now);
67
+ assertSecureUrl(account.tokenEndpoint, 'The token endpoint');
68
+ const res = await fetchImpl(account.tokenEndpoint, {
69
+ method: 'POST',
70
+ signal: AbortSignal.timeout(30_000),
71
+ headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
72
+ body: new URLSearchParams({
73
+ grant_type: TOKEN_EXCHANGE,
74
+ subject_token: account.accessToken,
75
+ subject_token_type: ACCESS_TOKEN_TYPE,
76
+ resource: opts.mcpUrl ?? DEFAULT_MCP_URL,
77
+ workspace_id: workspaceId,
78
+ client_id: account.clientId,
79
+ }),
80
+ });
81
+ const body = (await res.json().catch(() => ({})));
82
+ if (!res.ok || typeof body['access_token'] !== 'string') {
83
+ const said = typeof body['error_description'] === 'string' ? body['error_description'] : undefined;
84
+ throw new AccountError(said ??
85
+ `Could not connect to workspace ${workspaceId} (HTTP ${res.status}). Retrying may help if GigaRAG was down; ` +
86
+ 'otherwise run: gigarag workspaces, to check your account can still reach it, or gigarag login to sign in again.');
87
+ }
88
+ const fresh = {
89
+ type: 'oauth',
90
+ accessToken: body['access_token'],
91
+ refreshToken: typeof body['refresh_token'] === 'string' ? body['refresh_token'] : undefined,
92
+ expiresAt: typeof body['expires_in'] === 'number' ? now() + body['expires_in'] * 1000 : undefined,
93
+ clientId: account.clientId,
94
+ tokenEndpoint: account.tokenEndpoint,
95
+ };
96
+ // Hand back the stamped copy, not `fresh`: a bridge keeps this credential in memory, and one
97
+ // without a stamp would pass as current after a later sign-in and be written back on refresh.
98
+ const slot = workspaceSlot(workspaceId);
99
+ const credential = { ...fresh, gen: generation };
100
+ // Stored only if no sign-in happened meanwhile. Otherwise this call still gets a working token,
101
+ // stamped with the old generation, so nothing refreshes it back into the slot later.
102
+ if (currentGeneration() === generation)
103
+ writeSlot(slot, credential);
104
+ const ws = (body['workspace'] ?? {});
105
+ return { credential, workspace: { id: ws.id ?? workspaceId, name: ws.name ?? workspaceId, role: ws.role ?? '' } };
106
+ }
@@ -1,8 +1,7 @@
1
- import { readConfig } from '../config.js';
1
+ import { readConfig, updateConfig } from '../config.js';
2
2
  import { assertSecureUrl } from '../secureUrl.js';
3
3
  import { DEFAULT_ISSUER } from '../constants.js';
4
4
  import { deleteSecret, getSecret, setSecret } from '../secrets.js';
5
- const ACCOUNT = 'default';
6
5
  /**
7
6
  * The shape agent keys take, `gr_live_<region>_<32 base62>_<6 hex>`. Checked
8
7
  * loosely: a format that changes must not lock people out.
@@ -19,16 +18,50 @@ export function envKey(env = process.env) {
19
18
  const value = env['GIGARAG_API_KEY']?.trim();
20
19
  return value && !value.startsWith('${') ? value : undefined;
21
20
  }
22
- /** `GIGARAG_API_KEY` wins, so CI and containers need no login step. */
23
- export function loadCredential(env = process.env) {
24
- const fromEnv = envKey(env);
25
- if (fromEnv)
26
- return { type: 'key', key: fromEnv };
27
- const stored = getSecret(ACCOUNT);
21
+ /**
22
+ * Where each credential lives in the keychain.
23
+ *
24
+ * `account` holds the GigaRAG CLI's account sign-in, which lists workspaces and
25
+ * is exchanged for one token per workspace. Each workspace's token pair, or a
26
+ * pasted agent key for it, lives in its own slot. `default` is the single slot
27
+ * every version before this used, read so a machine signed in before still
28
+ * works until it signs in again.
29
+ *
30
+ * The slot is `workspace-<id>`: the file store names its file after the slot,
31
+ * and Windows refuses a colon in a filename.
32
+ */
33
+ export const ACCOUNT_SLOT = 'account';
34
+ export const LEGACY_SLOT = 'default';
35
+ export const workspaceSlot = (id) => `workspace-${id}`;
36
+ const isWorkspaceSlot = (slot) => slot.startsWith('workspace-');
37
+ /** The sign-in generation this machine is on, bumped by `clearWorkspaceSlots`. */
38
+ export function currentGeneration() {
39
+ return readConfig().authGeneration ?? 0;
40
+ }
41
+ /**
42
+ * True for a workspace slot stamped with a generation that isn't the current one: a credential a
43
+ * previous account's sign-in wrote or refreshed, left behind after a fresh browser sign-in cleared
44
+ * the slot it came from. Only workspace slots carry this check; the account and legacy slots are
45
+ * rewritten in full on every sign-in and are never subject to `clearWorkspaceSlots`.
46
+ */
47
+ function isStale(slot, credential) {
48
+ // An unstamped credential counts as generation 0: slots written before stamping existed stay
49
+ // readable until the first new sign-in, which deletes them anyway, and a bridge holding one in
50
+ // memory cannot write it back after that sign-in.
51
+ return isWorkspaceSlot(slot) && (credential.gen ?? 0) !== currentGeneration();
52
+ }
53
+ /** The credential as `writeSlot` stores it: stamped with this sign-in generation for a workspace slot. */
54
+ export function stampForSlot(slot, credential) {
55
+ return isWorkspaceSlot(slot) ? { ...credential, gen: currentGeneration() } : credential;
56
+ }
57
+ export function readSlot(slot) {
58
+ const stored = getSecret(slot);
28
59
  if (!stored)
29
60
  return undefined;
30
61
  try {
31
62
  const parsed = JSON.parse(stored.value);
63
+ if (isStale(slot, parsed))
64
+ return undefined;
32
65
  if (parsed.type === 'key' && parsed.key)
33
66
  return parsed;
34
67
  if (parsed.type === 'oauth' && parsed.accessToken)
@@ -39,17 +72,86 @@ export function loadCredential(env = process.env) {
39
72
  }
40
73
  return undefined;
41
74
  }
42
- /** Stores a credential and returns where it went, for the "connected" line. */
43
- export function saveCredential(credential) {
44
- return setSecret(ACCOUNT, JSON.stringify(credential));
75
+ /** Stores a credential in a slot and returns where it went, for the "connected" line. */
76
+ export function writeSlot(slot, credential) {
77
+ const stamped = stampForSlot(slot, credential);
78
+ const where = setSecret(slot, JSON.stringify(stamped));
79
+ updateConfig(c => {
80
+ const slots = new Set(c.credentialSlots ?? []);
81
+ slots.add(slot);
82
+ c.credentialSlots = [...slots];
83
+ });
84
+ return where;
85
+ }
86
+ export function clearSlot(slot) {
87
+ deleteSecret(slot);
88
+ }
89
+ /**
90
+ * Deletes every `workspace-<id>` slot, and the machine default and cached
91
+ * workspace names that go with them, without touching the account slot or the
92
+ * legacy one.
93
+ *
94
+ * Called before a fresh browser sign-in is stored. Without it, a workspace
95
+ * token minted for a previous account keeps working after a different person
96
+ * signs in on the same machine, because each slot is keyed by workspace id and
97
+ * a new account's own workspace could reuse an id the old one also happened to
98
+ * reach, or the stale slot is simply presented for a workspace the new account
99
+ * was never granted, either of which sends the new person's work to a
100
+ * workspace their sign-in never chose.
101
+ *
102
+ * Deleting the slots isn't enough by itself: a bridge process started before this
103
+ * runs can still hold one of them in memory, and its normal refresh-on-expiry
104
+ * logic would write the rotated pair straight back afterwards. So this also bumps
105
+ * `authGeneration`, and `refreshOAuth` refuses to write a workspace slot back
106
+ * under a generation that has moved on (see `isStale` above).
107
+ */
108
+ export function clearWorkspaceSlots() {
109
+ const config = readConfig();
110
+ const kept = [];
111
+ for (const slot of config.credentialSlots ?? []) {
112
+ if (isWorkspaceSlot(slot))
113
+ deleteSecret(slot);
114
+ else
115
+ kept.push(slot);
116
+ }
117
+ updateConfig(c => {
118
+ c.credentialSlots = kept;
119
+ c.workspaceId = undefined;
120
+ c.workspaceNames = undefined;
121
+ c.authGeneration = (c.authGeneration ?? 0) + 1;
122
+ });
123
+ }
124
+ export function loadAccountCredential() {
125
+ const credential = readSlot(ACCOUNT_SLOT);
126
+ return credential?.type === 'oauth' ? credential : undefined;
127
+ }
128
+ /**
129
+ * `GIGARAG_API_KEY` wins, so CI and containers need no login step. Then the
130
+ * workspace's own slot when a workspace is named, and the legacy slot only when
131
+ * none is: answering a workspace with another workspace's credential would send
132
+ * a person's memos to the wrong place without a word.
133
+ */
134
+ export function loadCredential(env = process.env, workspaceId) {
135
+ const fromEnv = envKey(env);
136
+ if (fromEnv)
137
+ return { type: 'key', key: fromEnv };
138
+ return readSlot(workspaceId ? workspaceSlot(workspaceId) : LEGACY_SLOT);
139
+ }
140
+ /** Stores a credential and returns where it went. The legacy slot unless told otherwise. */
141
+ export function saveCredential(credential, slot = LEGACY_SLOT) {
142
+ return writeSlot(slot, credential);
45
143
  }
144
+ /** Deletes every credential this machine holds: the account, each workspace, and the legacy slot. */
46
145
  export function clearCredential() {
47
- deleteSecret(ACCOUNT);
146
+ const slots = new Set([...(readConfig().credentialSlots ?? []), ACCOUNT_SLOT, LEGACY_SLOT]);
147
+ for (const slot of slots)
148
+ deleteSecret(slot);
149
+ updateConfig(c => void (c.credentialSlots = []));
48
150
  }
49
- export function credentialSource(env = process.env) {
151
+ export function credentialSource(env = process.env, slot = LEGACY_SLOT) {
50
152
  if (envKey(env))
51
153
  return 'the GIGARAG_API_KEY environment variable';
52
- return getSecret(ACCOUNT)?.store;
154
+ return getSecret(slot)?.store;
53
155
  }
54
156
  /** `gr_live_eu_*****`, for printing. */
55
157
  export function mask(key) {
@@ -72,7 +174,7 @@ export async function authorizationHeader(credential, fetchImpl = fetch, now = D
72
174
  }
73
175
  return `Bearer ${credential.accessToken}`;
74
176
  }
75
- export async function refreshOAuth(credential, fetchImpl = fetch, now = Date.now) {
177
+ export async function refreshOAuth(credential, fetchImpl = fetch, now = Date.now, slot = LEGACY_SLOT) {
76
178
  if (!credential.refreshToken)
77
179
  throw new Error('The sign-in has expired. Run: gigarag login');
78
180
  assertSecureUrl(credential.tokenEndpoint, 'The token endpoint');
@@ -98,7 +200,14 @@ export async function refreshOAuth(credential, fetchImpl = fetch, now = Date.now
98
200
  refreshToken: typeof body['refresh_token'] === 'string' ? body['refresh_token'] : credential.refreshToken,
99
201
  expiresAt: typeof body['expires_in'] === 'number' ? now() + body['expires_in'] * 1000 : undefined,
100
202
  };
101
- saveCredential(next);
203
+ // `credential` was loaded (or held in memory by a long-running bridge) under an older sign-in
204
+ // generation than this machine is now on: a fresh browser sign-in cleared this workspace slot
205
+ // and moved the generation forward since. Writing the refreshed pair back would resurrect the
206
+ // previous account's access for every later process that reads the slot, so it is dropped
207
+ // instead. The caller here still gets a working pair for this one call.
208
+ if (isStale(slot, credential))
209
+ return next;
210
+ saveCredential(next, slot);
102
211
  return next;
103
212
  }
104
213
  export function configuredIssuer() {
package/cli/binding.js ADDED
@@ -0,0 +1,117 @@
1
+ import { existsSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { readConfig, updateConfig } from './config.js';
4
+ import { findRepoRoot } from './scan/repo.js';
5
+ /**
6
+ * Which GigaRAG workspace a repository belongs to.
7
+ *
8
+ * `.gigarag.json` at the repository root names a workspace, and optionally the
9
+ * bucket the repository is indexed into. It holds no secret, so it can be
10
+ * committed, and a teammate's checkout then reaches the same workspace with
11
+ * their own sign-in. `gigarag use` writes it.
12
+ */
13
+ export const BINDING_FILE = '.gigarag.json';
14
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
15
+ const SLUG = /^[a-z0-9][a-z0-9-]{0,63}$/;
16
+ /** The binding above `start`, or undefined when there is none or it does not validate. */
17
+ export function readBinding(start) {
18
+ const { root } = findRepoRoot(start);
19
+ const path = join(root, BINDING_FILE);
20
+ if (!existsSync(path))
21
+ return undefined;
22
+ try {
23
+ const text = readFileSync(path, 'utf8');
24
+ const raw = JSON.parse(text.charCodeAt(0) === 0xfeff ? text.slice(1) : text);
25
+ if (typeof raw.workspace !== 'string' || !UUID.test(raw.workspace))
26
+ return undefined;
27
+ const binding = { workspace: raw.workspace.toLowerCase() };
28
+ if (typeof raw.workspaceName === 'string')
29
+ binding.workspaceName = raw.workspaceName.slice(0, 200);
30
+ if (typeof raw.bucket === 'string' && SLUG.test(raw.bucket))
31
+ binding.bucket = raw.bucket;
32
+ return { binding, root };
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ }
38
+ /** Writes the binding at `root`, atomically, and returns its path. */
39
+ export function writeBinding(root, binding) {
40
+ const path = join(root, BINDING_FILE);
41
+ const tmp = `${path}.${process.pid}.tmp`;
42
+ writeFileSync(tmp, `${JSON.stringify(binding, null, 2)}\n`);
43
+ renameSync(tmp, path);
44
+ return path;
45
+ }
46
+ /**
47
+ * The repository root, canonicalised the way trust is keyed: symlinks and
48
+ * junctions resolved, and lowercased on Windows, where the same folder can be
49
+ * reached under more than one case. Trust must not be foolable by either.
50
+ */
51
+ function trustKey(root) {
52
+ const real = realpathSync(root);
53
+ return process.platform === 'win32' ? real.toLowerCase() : real;
54
+ }
55
+ /**
56
+ * Whether `root`'s binding to `workspace` has been trusted: recorded when the
57
+ * person ran `gigarag use` or `gigarag trust` for this exact pair. Trust-on-
58
+ * first-use, the way `direnv` treats a new `.envrc`, because `.gigarag.json` is
59
+ * committed and read before anyone has reviewed it, and it otherwise silently
60
+ * redirects a clone to whatever workspace the file names, including one an
61
+ * attacker invited the victim into.
62
+ */
63
+ export function isTrusted(root, workspace) {
64
+ return readConfig().trustedBindings?.[trustKey(root)] === workspace;
65
+ }
66
+ /**
67
+ * The binding at `root`, but only once it has been trusted; an untrusted one reads exactly like
68
+ * no binding at all. `repoInfo` (`scan/repo.ts`) and `syncSlug` (`sync/slug.ts`) both resolve a
69
+ * bucket slug and workspace through this one function so they cannot drift on what "trusted"
70
+ * means: before this existed, `repoInfo` read `readBinding` directly and skipped the trust check
71
+ * entirely, so a committed `.gigarag.json` an attacker pointed at another of the victim's buckets
72
+ * was followed on sight by `/gigaindex`, `gigarag record` and the PostToolUse dirty-marking hook.
73
+ */
74
+ export function trustedBinding(root) {
75
+ const found = readBinding(root);
76
+ if (!found || !isTrusted(found.root, found.binding.workspace))
77
+ return undefined;
78
+ return found;
79
+ }
80
+ /** Records that `root`'s binding to `workspace` is trusted. A changed workspace id needs trusting again. */
81
+ export function trustBinding(root, workspace) {
82
+ const key = trustKey(root);
83
+ updateConfig(c => {
84
+ c.trustedBindings = { ...(c.trustedBindings ?? {}), [key]: workspace };
85
+ });
86
+ }
87
+ /** The binding above `cwd`, when there is one but it has not been trusted yet. */
88
+ export function untrustedBinding(cwd = process.cwd()) {
89
+ const found = readBinding(cwd);
90
+ if (!found || isTrusted(found.root, found.binding.workspace))
91
+ return undefined;
92
+ return { workspace: found.binding.workspace, root: found.root };
93
+ }
94
+ /** The one-line notice for an untrusted binding: which workspace it names, and the exact command to trust it. */
95
+ export function untrustedBindingNotice(untrusted) {
96
+ return (`${BINDING_FILE} at ${untrusted.root} links this folder to workspace ${untrusted.workspace}, which has not been trusted, ` +
97
+ 'so GigaRAG used the default workspace instead. If you placed or reviewed this file yourself, run: gigarag trust');
98
+ }
99
+ /**
100
+ * The workspace a process in `cwd` should use: `GIGARAG_WORKSPACE`, then the
101
+ * repository's binding when it has been trusted, then the machine default that
102
+ * `gigarag login` set. An untrusted binding is treated as absent here; callers
103
+ * that want to tell the person about it call `untrustedBinding` themselves,
104
+ * since a library call and a background hook must not print anything, and a
105
+ * bridge or hook is what decides where that notice goes.
106
+ */
107
+ export function resolveWorkspace(cwd = process.cwd(), env = process.env) {
108
+ const fromEnv = env['GIGARAG_WORKSPACE']?.trim();
109
+ if (fromEnv && UUID.test(fromEnv))
110
+ return { id: fromEnv.toLowerCase(), source: 'env' };
111
+ const found = readBinding(cwd);
112
+ if (found && isTrusted(found.root, found.binding.workspace)) {
113
+ return { id: found.binding.workspace, source: 'binding', binding: found.binding };
114
+ }
115
+ const fallback = readConfig().workspaceId;
116
+ return fallback && UUID.test(fallback) ? { id: fallback.toLowerCase(), source: 'default' } : undefined;
117
+ }
package/cli/cli.js CHANGED
@@ -8,10 +8,15 @@ Connect GigaRAG to your AI clients.
8
8
  gigarag logout Delete the stored credential
9
9
  gigarag connect [client...] Add GigaRAG to your installed clients
10
10
  gigarag status What is configured, and whether the key works
11
+ gigarag workspaces Every workspace your account can reach
12
+ gigarag use <workspace> Link this repository to a workspace
13
+ gigarag trust [path] Trust a .gigarag.json a teammate already committed
11
14
 
12
15
  gigarag scan [path] What changed in a repo since it was last indexed
13
16
  gigarag repo [path] Which bucket a directory belongs to
14
17
  gigarag record Tell the local index what was indexed (reads JSON on stdin)
18
+ gigarag hooks install [path] Git hooks that keep GigaRAG in step after each commit
19
+ gigarag sync begin|end|status Keep a repository's memos in step with git (/gigasync runs it)
15
20
 
16
21
  gigarag mcp The stdio bridge a client's config runs
17
22
  gigarag auth-header The auth header, for headersHelper
@@ -24,9 +29,14 @@ const COMMANDS = {
24
29
  logout: async () => (await import('./commands/login.js')).logout,
25
30
  connect: async () => (await import('./commands/connect.js')).connect,
26
31
  status: async () => (await import('./commands/status.js')).status,
32
+ workspaces: async () => (await import('./commands/workspaces.js')).workspaces,
33
+ use: async () => (await import('./commands/use.js')).use,
34
+ trust: async () => (await import('./commands/trust.js')).trust,
27
35
  scan: async () => (await import('./commands/scan.js')).scan,
28
36
  repo: async () => (await import('./commands/repo.js')).repo,
29
37
  record: async () => (await import('./commands/record.js')).record,
38
+ hooks: async () => (await import('./commands/gitHooks.js')).hooks,
39
+ sync: async () => (await import('./commands/sync.js')).sync,
30
40
  mcp: async () => (await import('./commands/mcp.js')).mcp,
31
41
  'auth-header': async () => (await import('./commands/authHeader.js')).authHeader,
32
42
  'index-sync': async () => (await import('./commands/indexSync.js')).indexSync,
@@ -1,18 +1,15 @@
1
- import { authorizationHeader, loadCredential } from '../auth/credentials.js';
1
+ import { createClient } from '../mcp/session.js';
2
2
  import { err, out } from '../ui.js';
3
3
  /**
4
4
  * What `headersHelper` runs. Claude Code expects one JSON object of string
5
5
  * pairs on stdout, and gives up after 10 seconds, so nothing else may print here
6
- * and nothing slow may run.
6
+ * and nothing slow may run. The workspace comes from the folder Claude Code
7
+ * starts it in, the same way the bridge picks it.
7
8
  */
8
9
  export async function authHeader() {
9
- const credential = loadCredential();
10
- if (!credential) {
11
- err('Not signed in to GigaRAG. Run: gigarag login');
12
- return 1;
13
- }
14
10
  try {
15
- out(JSON.stringify({ Authorization: await authorizationHeader(credential) }));
11
+ const authorization = await createClient().authorizationHeader();
12
+ out(JSON.stringify({ Authorization: authorization }));
16
13
  return 0;
17
14
  }
18
15
  catch (e) {
@@ -1,6 +1,7 @@
1
1
  import { parseArgs } from 'node:util';
2
2
  import { updateConfig, readConfig } from '../config.js';
3
- import { loadCredential } from '../auth/credentials.js';
3
+ import { loadAccountCredential, loadCredential } from '../auth/credentials.js';
4
+ import { resolveWorkspace } from '../binding.js';
4
5
  import { resolveUrl } from '../mcp/session.js';
5
6
  import { tildify } from '../paths.js';
6
7
  import { connectClient, removeClientEntry, renderConfig } from '../clients/connect.js';
@@ -168,7 +169,8 @@ export async function connect(argv) {
168
169
  }
169
170
  // With --json the output is for a program, and one stray line after the JSON would break its parser.
170
171
  if (!values.json) {
171
- if (!loadCredential()) {
172
+ const signedIn = loadCredential(process.env, resolveWorkspace()?.id) || loadAccountCredential();
173
+ if (!signedIn) {
172
174
  out('\nYou are not signed in yet, so these clients cannot reach GigaRAG. Run: gigarag login');
173
175
  }
174
176
  if (named.length === 0) {