agent-carry 0.4.0 → 0.4.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.
package/CHANGELOG.md CHANGED
@@ -1,18 +1,33 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ Plugin, marketplace entry and package share one version number; `npm version` keeps them in step.
4
4
 
5
- - `carry list --all` now shows the most recent sessions across every folder, newest first, with the title the desktop app shows and the folder; background-agent sessions (e.g. a memory plugin's observer) are hidden unless `--everything`. `carry export` with no id offers a numbered picker. `--limit N|all` shows more, `--project DIR` lists one folder, `--match TEXT` filters by title, folder or id, and `--desktop` / `--terminal` keep only sessions started in the desktop app or a terminal (every row is tagged; archived desktop sessions are marked; a desktop session with no project folder shows as "desktop scratch folder"). Nobody needs to know where sessions are stored. `/carry:share` uses it (plugin 0.3.0).
5
+ ## 0.4.1
6
+ - `/carry:pickup` no longer asks which account to use or shows ids. `--desktop` picks the account the desktop app is signed in to (read from the app's own config, with the log as a fallback), and in a terminal asks only when the terminal and the desktop app are logged in as different accounts.
7
+ - Accounts are described by name and email where known (the terminal's login), otherwise by the sessions they hold, never by id: `carry desktop-dirs` and the import output now say which account's Code tab got the session.
6
8
 
7
- - Plugin 0.2.1 (released): commands check the CLI version first and say how to upgrade if it is older, instead of aborting on an unknown-command error.
8
- - Plugin 0.2.0: `/carry:share` and `/carry:pickup` ask what they need (session, encryption, passphrase source, destination folder, desktop app and account) instead of expecting flags. Needs the CLI from this release.
9
+ ## 0.4.0
10
+
11
+ Finding sessions without knowing where they are stored. (0.3.0 was tagged but never published to npm.)
12
+
13
+ - `carry list --all` shows the most recent sessions across every folder, newest first, each tagged by where it was started (`[desktop]`, `[terminal]`, `[print mode]`) with the title the desktop app shows. Background-agent sessions (for example a memory plugin's observer) are hidden unless `--everything`.
14
+ - `carry list` options: `--limit N|all`, `--project DIR` (one folder), `--match TEXT` (title, folder or id), `--desktop` / `--terminal` (only sessions started there). Archived desktop sessions are marked; a desktop conversation with no project folder shows as "desktop scratch folder".
15
+ - `carry export` with no id offers a numbered picker that takes the same options.
16
+ - `/carry:share` uses the list; the plugin commands need CLI 0.4.0 or newer.
17
+
18
+ ## 0.2.1
19
+
20
+ - Plugin commands check the CLI version first and say how to upgrade if it is older, instead of aborting on an unknown-command error.
21
+
22
+ ## 0.2.0
23
+
24
+ - `carry import --desktop` adds the session to the Claude desktop app's Code tab (macOS). It targets the signed-in account (read from the app's log) and works for a freshly signed-in account by borrowing only app-level settings. Terminal colour codes in newly imported text are stripped, since the app shows them as literal characters.
25
+ - `export --latest` skips sessions with no prompts.
26
+ - Interactive plugin commands: `/carry:share` and `/carry:pickup` ask what they need (session, encryption, passphrase source, destination folder, desktop app and account). Installable with `/plugin install carry --marketplace harshhh28/agent-carry`.
9
27
  - `export --generate-passphrase` (saved to `FILE.pass`, never printed); `carry bundles`; `carry desktop-dirs`.
10
- - `--desktop` targets the signed-in account (read from the app's log) instead of any folder with sessions, and works for a freshly signed-in account by borrowing only app-level settings.
11
28
 
12
- - Plugin installable from this repo: `/plugin install carry --marketplace harshhh28/agent-carry`.
13
- - `export --latest` skips sessions with no prompts, so an opened-and-closed session no longer shadows the real one.
14
- - `--desktop` strips terminal colour codes from newly imported text, which the app shows as literal characters.
15
- - `carry import --desktop` adds the session to the Claude desktop app's Code tab (macOS).
29
+ ## 0.1.1
30
+
16
31
  - `agent-carry` works as a second command name for `carry`.
17
32
  - README trimmed; plugin author set.
18
33
 
package/README.md CHANGED
@@ -42,7 +42,7 @@ carry import session.carry.gz --desktop
42
42
  App settings are copied from that account's newest Code-tab session (or, for a new account, another account's), so use the Code tab at least once first.
43
43
  Terminal colour codes in the imported text are stripped (the app would show them as literal `[38;2;…m`). Restart the desktop app if the session doesn't show.
44
44
 
45
- The session is added to the account that is signed in (read from the app's log). If it can't tell, or you want another account, use `--desktop-dir DIR`; `carry desktop-dirs` lists the choices. A freshly signed-in account with no sessions borrows only app-level settings from another account, never its connectors or permissions. Cowork is a different store and isn't supported.
45
+ The session is added to the account the desktop app is signed in to. From a terminal that is logged in as a different account, `carry` stops and asks which Code tab you mean, naming the accounts (by name and email where known, otherwise by the sessions they hold). To choose explicitly use `--desktop-dir DIR`; `carry desktop-dirs` lists the accounts. A freshly signed-in account with no sessions borrows only app-level settings from another account, never its connectors or permissions. Cowork is a different store and isn't supported.
46
46
 
47
47
  ### Sync through a shared folder
48
48
 
@@ -61,7 +61,7 @@ Projects are matched by git remote, not path. Each file has one writer, so there
61
61
  /plugin install carry --marketplace harshhh28/agent-carry
62
62
  ```
63
63
 
64
- Needs `npm i -g agent-carry` (or falls back to `npx`). Works the same in the terminal and in the desktop app's Code tab.
64
+ Needs the CLI (`npm i -g agent-carry`, 0.4.0 or newer). Works the same in the terminal and in the desktop app's Code tab. To update later: `npm i -g agent-carry@latest`, then `/plugin marketplace update agent-carry` and `/reload-plugins`.
65
65
 
66
66
  - `/carry:share` asks which session, whether to encrypt (generate a passphrase, use your own file, or type one), and where to save.
67
67
  - `/carry:pickup` lists bundles in this folder, ~/Downloads and ~/Desktop, then asks which one, the passphrase source, which folder to import into, and whether to add it to the desktop app (and which account).
@@ -73,7 +73,7 @@ Passphrases are never put on a command line. A generated one is saved beside the
73
73
  | command | |
74
74
  |---|---|
75
75
  | `carry list [--all] [--project DIR] [--desktop\|--terminal] [--match TEXT] [--limit N]` | sessions for this folder, or `--all`: recent sessions from every folder, each tagged by where it was started (`[desktop]`, `[terminal]`, `[print mode]`) with the desktop app's title. `--desktop` / `--terminal` keep only those. Background-agent sessions are hidden; `--everything` shows them |
76
- | `carry export [id\|--latest] [-o FILE]` | with no id, pick from a numbered list (takes the same `--desktop`, `--terminal`, `--project`, `--match`). `--generate-passphrase`, `--no-redact`, `--redact-emails`, `--strip-thinking`, `--allow-thinking-secrets`, `--project-root DIR` |
76
+ | `carry export [id\|--latest] [-o FILE]` | with no id, pick from a numbered list (takes the same `--desktop`, `--terminal`, `--project`, `--match`). Also `--generate-passphrase`, `--no-redact`, `--redact-emails`, `--strip-thinking`, `--allow-thinking-secrets`, `--project-root DIR` |
77
77
  | `carry inspect FILE` | header, parts, redaction counts |
78
78
  | `carry import FILE [--project-root DIR] [--dry-run]` | write into the target folder's session store |
79
79
  | `carry bundles` / `carry desktop-dirs` | bundle files nearby; desktop-app account folders |
@@ -92,7 +92,7 @@ Passphrases are never put on a command line. A generated one is saved beside the
92
92
  ## Development
93
93
 
94
94
  ```
95
- npm test # 49 tests, synthetic two-machine setups
95
+ npm test # 56 tests, synthetic two-machine setups
96
96
  npm run check:real # read-only round trip of your own ~/.claude sessions
97
97
  ```
98
98
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-carry",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Move coding-agent sessions between machines and folders: path-portable, redacted, encrypted, merge-safe bundles. Claude Code today; adapter-ready for Codex and others.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,9 @@
20
20
  },
21
21
  "scripts": {
22
22
  "test": "node --test \"test/*.test.js\"",
23
- "check:real": "node scripts/check-real.js"
23
+ "check:real": "node scripts/check-real.js",
24
+ "preversion": "npm test",
25
+ "version": "node scripts/sync-version.js && git add plugin/.claude-plugin/plugin.json .claude-plugin/marketplace.json CHANGELOG.md"
24
26
  },
25
27
  "keywords": [
26
28
  "claude-code",
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "carry",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Move coding-agent sessions between machines and folders: /carry:share exports a redacted, path-portable bundle, /carry:pickup imports one.",
5
- "author": { "name": "Harsh Gajjar", "email": "harshgajjar2812@gmail.com" },
5
+ "author": {
6
+ "name": "Harsh Gajjar",
7
+ "email": "harshgajjar2812@gmail.com"
8
+ },
6
9
  "license": "MIT"
7
10
  }
@@ -4,7 +4,7 @@ argument-hint: [bundle-file]
4
4
  allowed-tools: Bash(carry:*), Bash(npx -y agent-carry@latest:*), Bash(ls:*), Bash(pwd), AskUserQuestion
5
5
  ---
6
6
 
7
- carry CLI version (this command needs 0.2.0 or newer): !`carry --version 2>&1 || echo 'not installed'`
7
+ carry CLI version (this command needs 0.4.0 or newer): !`carry --version 2>&1 || echo 'not installed'`
8
8
 
9
9
  Bundle files nearby (this folder, ~/Downloads, ~/Desktop), newest first:
10
10
 
@@ -12,13 +12,13 @@ Bundle files nearby (this folder, ~/Downloads, ~/Desktop), newest first:
12
12
 
13
13
  Current folder: !`pwd`
14
14
 
15
- Claude desktop app account folders (macOS only; an error here just means no desktop app):
15
+ Claude desktop app accounts (macOS only; an error here just means no desktop app):
16
16
 
17
17
  !`carry desktop-dirs 2>&1 || true`
18
18
 
19
19
  Arguments the user typed: $ARGUMENTS
20
20
 
21
- **Step 0.** Look at the version line above. If it says "not installed" or shows anything older than 0.2.0, stop and say exactly this, with nothing else: "This command needs the carry CLI 0.2.0 or newer. Run `npm i -g agent-carry@latest`, then try again." (If that install still gives an older version, 0.2.0 has not been published to npm yet.) Any error text shown in the listings below comes from the same cause; do not try to work around it.
21
+ **Step 0.** Look at the version line above. If it says "not installed" or shows anything older than 0.4.0, stop and say exactly this, with nothing else: "This command needs the carry CLI 0.4.0 or newer. Run `npm i -g agent-carry@latest`, then try again." (If that install still gives an older version, 0.4.0 has not been published to npm yet.) Any error text shown in the listings below comes from the same cause; do not try to work around it.
22
22
 
23
23
  You are importing one session with the `carry` CLI.
24
24
 
@@ -31,6 +31,6 @@ Ask with the AskUserQuestion tool, one short question at a time, and skip any qu
31
31
  - "I'll type it here": warn that it is stored in this conversation's history. Pass it as `CARRY_PASSPHRASE='…' carry import …` and never repeat it back.
32
32
  (If `CARRY_PASSPHRASE` is already set in the environment, nothing is needed.) On a wrong-passphrase error, say so and ask once more.
33
33
  3. **Which folder.** Claude Code finds a session by the folder you run `claude` in, so this should be the folder they will work in. Offer the current folder as "(Recommended)" or another path (create it with `mkdir -p` only if the user says so). Pass it as `--project-root`.
34
- 4. **Claude desktop app (only if account folders are listed above).** Ask "Also show it in the Claude desktop app's Code tab?" If yes and more than one folder is listed, ask which account: the one marked "* signed in" is "(Recommended)"; the rest are listed by their folder name and session count. Use `--desktop` for the signed-in account, or `--desktop-dir <folder>` for another one. If the import says it cannot tell which account is signed in, ask the user to choose from the list it printed.
34
+ 4. **Claude desktop app (only if account folders are listed above).** Ask only "Also show it in the Claude desktop app's Code tab?" Do not ask which account: `--desktop` picks the right one by itself (the account the desktop app is signed in to; in a terminal, the one that matches this terminal's login). Never show the user folder paths or ids. If the import fails with "this terminal is logged in as one Claude account but the desktop app is signed in as another", or "cannot tell which desktop account is signed in", ask once with AskUserQuestion, labelling each option with the account name and email, session count and example titles taken from that error (never ids or paths), and re-run with `--desktop-dir <that option's folder>`. A line starting with `*` in the listing above marks the account the desktop app is signed in to.
35
35
  5. **Run** `carry import <bundle> --project-root <folder> [--passphrase-file F] [--desktop | --desktop-dir D]` (add `--dry-run` if the user typed it, and say nothing was written).
36
- 6. **Report in at most five lines:** the session id, the folder it went into, and any warning. Then the next step: exit this session, then `cd <folder> && claude --resume <id>`; for the desktop app, restart it if the session does not appear and open the Code tab. A running session cannot swap its own history, so do not run `claude --resume` yourself.
36
+ 6. **Report in at most five lines:** the session id, the folder it went into, which account's Code tab it was added to (use the "Code tab of:" line from the import, e.g. a name and email; say it can be redone for another account if that is wrong), and any warning. Then the next step: exit this session, then `cd <folder> && claude --resume <id>`; for the desktop app, restart it if the session does not appear and open the Code tab. A running session cannot swap its own history, so do not run `claude --resume` yourself.
@@ -4,7 +4,7 @@ argument-hint: [optional carry export flags, e.g. -o FILE --strip-thinking]
4
4
  allowed-tools: Bash(carry:*), Bash(npx -y agent-carry@latest:*), AskUserQuestion
5
5
  ---
6
6
 
7
- carry CLI version (this command needs 0.2.0 or newer): !`carry --version 2>&1 || echo 'not installed'`
7
+ carry CLI version (this command needs 0.4.0 or newer): !`carry --version 2>&1 || echo 'not installed'`
8
8
 
9
9
  Recent sessions from every folder, newest first, including ones started in the Claude desktop app (the top one is usually this conversation):
10
10
 
@@ -14,7 +14,7 @@ Current folder: !`pwd`
14
14
 
15
15
  Arguments the user typed: $ARGUMENTS
16
16
 
17
- **Step 0.** Look at the version line above. If it says "not installed" or shows anything older than 0.2.0, stop and say exactly this, with nothing else: "This command needs the carry CLI 0.2.0 or newer. Run `npm i -g agent-carry@latest`, then try again." (If that install still gives an older version, 0.2.0 has not been published to npm yet.) Any error text shown in the listings below comes from the same cause; do not try to work around it.
17
+ **Step 0.** Look at the version line above. If it says "not installed" or shows anything older than 0.4.0, stop and say exactly this, with nothing else: "This command needs the carry CLI 0.4.0 or newer. Run `npm i -g agent-carry@latest`, then try again." (If that install still gives an older version, 0.4.0 has not been published to npm yet.) Any error text shown in the listings below comes from the same cause; do not try to work around it.
18
18
 
19
19
  You are exporting one session with the `carry` CLI.
20
20
 
package/src/cli.js CHANGED
@@ -219,7 +219,10 @@ function main() {
219
219
 
220
220
  if (cmd === 'desktop-dirs') {
221
221
  const list = listDesktopDirs({ home: userHome });
222
- for (const x of list) process.stdout.write(`${x.active ? '* signed in' : ' other '} ${x.entries} session${x.entries === 1 ? ' ' : 's'} ${x.dir}\n`);
222
+ for (const x of list) {
223
+ const tags = [x.active ? 'signed in to the desktop app' : null, x.cli ? 'this terminal\'s login' : null].filter(Boolean).join(', ');
224
+ process.stdout.write(`${x.active ? '*' : ' '} ${x.label}${tags ? ` [${tags}]` : ''}\n --desktop-dir "${x.dir}"\n`);
225
+ }
223
226
  if (!list.length) process.stdout.write('no desktop account folders found\n');
224
227
  return;
225
228
  }
@@ -248,7 +251,7 @@ function main() {
248
251
  const d = registerDesktopSession({ id: plan.id, root: plan.root, mainFile: main.dest, home: userHome, dir: flags['desktop-dir'], dryRun: !!flags['dry-run'] });
249
252
  const verb = { registered: 'added to the desktop Code tab', exists: 'already in the desktop Code tab', 'would-register': 'would be added to the desktop Code tab' }[d.status];
250
253
  process.stdout.write(` ${verb} (${d.file}); restart the Claude desktop app if it does not appear\n`);
251
- process.stdout.write(` desktop folder: ${d.dir}\n`);
254
+ if (d.label) process.stdout.write(` Code tab of: ${d.label}\n`);
252
255
  if (d.status === 'exists' && d.entryCwd && d.entryCwd !== plan.root) process.stdout.write(` note: that Code-tab entry points at ${d.entryCwd}, not ${plan.root}\n`);
253
256
  process.stdout.write(` chosen as: ${d.how}${d.borrowed ? '; this account has no sessions yet, so app settings were borrowed from another account' : ''}\n`);
254
257
  } catch (e) {
package/src/desktop.js CHANGED
@@ -59,37 +59,101 @@ export function activeAccountFromLog(home, platform = process.platform) {
59
59
  }
60
60
  }
61
61
 
62
- /** Every <root>/<account>/<org> folder, with how many sessions it holds and whether it is the signed-in one. */
62
+ /** The account the Claude Code CLI is logged in to, with its readable name, from ~/.claude.json. */
63
+ export function cliAccount(userHome) {
64
+ try {
65
+ const j = JSON.parse(fs.readFileSync(path.join(userHome, '.claude.json'), 'utf8'));
66
+ const a = j.oauthAccount;
67
+ if (!a?.accountUuid) return null;
68
+ return { account: a.accountUuid, org: a.organizationUuid ?? null, name: a.fullName || a.displayName || null, email: a.emailAddress ?? null, orgName: a.organizationName && !(a.emailAddress && a.organizationName.includes(a.emailAddress)) ? a.organizationName : null };
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+
74
+ /** The account the desktop app is signed in to: its own config.json records the last one. */
75
+ export function desktopSignedInAccount(home, platform = process.platform) {
76
+ if (platform !== 'darwin') return null;
77
+ try {
78
+ const j = JSON.parse(fs.readFileSync(path.join(home, 'Library', 'Application Support', 'Claude', 'config.json'), 'utf8'));
79
+ return typeof j.lastKnownAccountUuid === 'string' ? j.lastKnownAccountUuid : null;
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ const plural = (n, w) => `${n} ${w}${n === 1 ? '' : 's'}`;
86
+
87
+ /**
88
+ * Every <root>/<account>/<org> folder, with a readable label. The app stores accounts by id only, so a name is
89
+ * known for the account this machine's Claude Code CLI is logged in to; any other account is described by how
90
+ * many sessions it holds and a title or two, which is what a person can recognise.
91
+ * `active`: the account the desktop app is signed in to. `cli`: the account the CLI uses.
92
+ */
63
93
  export function listDesktopDirs({ home, platform }) {
64
94
  const root = desktopRoot(home, platform);
65
95
  if (!fs.existsSync(root)) throw new Error(`no Claude desktop Code-tab data at ${root}; open the Code tab once first`);
66
- const active = activeAccountFromLog(home, platform);
67
- return subdirs(root).flatMap((a) =>
96
+ const me = cliAccount(home);
97
+ const leaves = subdirs(root).flatMap((a) =>
68
98
  subdirs(a).map((d) => {
69
- const files = fs.readdirSync(d).filter((f) => /^local_.*\.json$/.test(f));
70
- const newest = Math.max(0, ...files.map((f) => fs.statSync(path.join(d, f)).mtimeMs));
71
- return { dir: d, account: path.basename(a), org: path.basename(d), entries: files.length, newest, active: !!active && active.account === path.basename(a) && active.org === path.basename(d) };
99
+ const entries = readEntries(d);
100
+ const files = entries.length;
101
+ const newest = Math.max(0, ...fs.readdirSync(d).filter((f) => /^local_.*\.json$/.test(f)).map((f) => fs.statSync(path.join(d, f)).mtimeMs));
102
+ const account = path.basename(a);
103
+ const org = path.basename(d);
104
+ const isMe = !!me && me.account === account;
105
+ const titles = entries.filter(({ json }) => json.title && !json.isArchived).sort((x, y) => (y.json.lastActivityAt || 0) - (x.json.lastActivityAt || 0)).map(({ json }) => String(json.title).slice(0, 40)).filter((t, i, all) => all.indexOf(t) === i).slice(0, 2).map((t) => `“${t}”`);
106
+ const who = isMe ? [me.name, me.email && `<${me.email}>`].filter(Boolean).join(' ') || 'This Claude Code login' : 'A different Claude account';
107
+ const orgName = isMe && me.org === org && me.orgName ? ` · ${me.orgName}` : '';
108
+ const label = `${who}${orgName} (${files === 0 ? 'no sessions in the Code tab yet' : `${plural(files, 'session')} in the Code tab`}${titles.length ? `, e.g. ${titles.join(', ')}` : ''})`;
109
+ return { dir: d, account, org, entries: files, newest, cli: isMe, name: isMe ? me.name : null, email: isMe ? me.email : null, label };
72
110
  }),
73
111
  );
112
+ // The signed-in desktop account (config.json; the app log as a fallback), and within it the org in use.
113
+ const log = activeAccountFromLog(home, platform);
114
+ const acct = desktopSignedInAccount(home, platform) ?? log?.account ?? null;
115
+ let activeDir = null;
116
+ if (acct) {
117
+ const mine = leaves.filter((x) => x.account === acct);
118
+ const pick = (log && log.account === acct && mine.find((x) => x.org === log.org)) || (me && me.account === acct && mine.find((x) => x.org === me.org)) || [...mine].sort((x, y) => y.newest - x.newest)[0];
119
+ activeDir = pick?.dir ?? null;
120
+ }
121
+ return leaves.map((x) => ({ ...x, active: x.dir === activeDir }));
74
122
  }
75
123
 
76
124
  /**
77
- * Which folder to write to: --desktop-dir if given, else the signed-in account/org (from the app log), else
78
- * the only folder if there is just one. With several and no signal it refuses rather than guess, because a
79
- * session written into another account's folder would never be seen.
125
+ * Which folder to write to, so nobody has to know account ids:
126
+ * - --desktop-dir if given;
127
+ * - in the desktop app, or when only one account could be meant: the account the desktop app is signed in to;
128
+ * - in a terminal: that same account if the terminal is logged in to it, but if the terminal and the desktop app
129
+ * are logged in as different accounts, ask (the error lists them by name), because a session written into the
130
+ * wrong account's Code tab would never be seen.
131
+ * `surface` is CLAUDE_CODE_ENTRYPOINT: 'cli' for a terminal, 'claude-desktop' for the desktop app.
80
132
  */
81
- export function resolveDesktopDir({ home, dir, platform }) {
133
+ export function resolveDesktopDir({ home, dir, platform, surface = process.env.CLAUDE_CODE_ENTRYPOINT }) {
82
134
  if (dir) {
83
135
  if (!fs.existsSync(dir)) throw new Error(`desktop folder not found: ${dir}`);
84
- return { dir: path.resolve(dir), how: 'given' };
136
+ return { dir: path.resolve(dir), how: 'chosen by you', label: null };
85
137
  }
86
138
  const all = listDesktopDirs({ home, platform });
87
139
  if (all.length === 0) throw new Error('no account folder in the desktop app data; open the Code tab once first');
88
- const active = all.find((x) => x.active);
89
- if (active) return { dir: active.dir, how: 'signed-in account (from the app log)' };
90
- if (all.length === 1) return { dir: all[0].dir, how: 'only account folder' };
91
- const rows = all.map((x) => ` ${x.dir} (${x.entries} session${x.entries === 1 ? '' : 's'})`).join('\n');
92
- throw new Error(`cannot tell which desktop account is signed in; pick one with --desktop-dir:\n${rows}`);
140
+ const desk = all.find((x) => x.active);
141
+ const mine = all.filter((x) => x.cli);
142
+ const cli = mine.find((x) => x.active) ?? [...mine].sort((x, y) => y.newest - x.newest)[0];
143
+ if (surface === 'cli' && cli && desk && cli.account !== desk.account) {
144
+ const e = new Error(
145
+ `this terminal is logged in as one Claude account but the desktop app is signed in as another; which Code tab should get the session?\n` +
146
+ ` 1. ${cli.label} (--desktop-dir "${cli.dir}")\n 2. ${desk.label} (--desktop-dir "${desk.dir}")`,
147
+ );
148
+ e.choices = [cli, desk];
149
+ throw e;
150
+ }
151
+ const chosen = desk ?? (surface === 'cli' ? cli : null) ?? (all.length === 1 ? all[0] : null);
152
+ if (chosen) return { dir: chosen.dir, how: chosen === desk ? 'the signed-in desktop account' : 'the only account folder', label: chosen.label };
153
+ const rows = all.map((x, i) => ` ${i + 1}. ${x.label} (--desktop-dir "${x.dir}")`).join('\n');
154
+ const e = new Error(`cannot tell which desktop account is signed in; choose one:\n${rows}`);
155
+ e.choices = all;
156
+ throw e;
93
157
  }
94
158
 
95
159
  function readEntries(dir) {
@@ -162,10 +226,10 @@ export function buildDesktopEntry({ template, cliSessionId, root, transcript, no
162
226
  * Returns { status: 'registered' | 'exists' | 'would-register', file, dir }.
163
227
  */
164
228
  export function registerDesktopSession({ id, root, mainFile, home, dir, platform, dryRun = false }) {
165
- const { dir: target, how } = resolveDesktopDir({ home, dir, platform });
229
+ const { dir: target, how, label } = resolveDesktopDir({ home, dir, platform });
166
230
  const existing = readEntries(target);
167
231
  const have = existing.find(({ json }) => json.cliSessionId === id || (Array.isArray(json.priorCliSessionIds) && json.priorCliSessionIds.includes(id)));
168
- if (have) return { status: 'exists', file: have.file, dir: target, how, entryCwd: have.json.cwd };
232
+ if (have) return { status: 'exists', file: have.file, dir: target, how, label, entryCwd: have.json.cwd };
169
233
  const newest = (list) => list.filter(({ json }) => !json.isArchived).sort((a, b) => (b.json.lastActivityAt || 0) - (a.json.lastActivityAt || 0))[0]?.json;
170
234
  let template = newest(existing);
171
235
  let borrowed = false;
@@ -179,7 +243,7 @@ export function registerDesktopSession({ id, root, mainFile, home, dir, platform
179
243
  }
180
244
  }
181
245
  if (!template) throw new Error('no existing Code-tab session anywhere to copy app settings from; start one session in the desktop app Code tab first');
182
- if (dryRun) return { status: 'would-register', file: 'a new local_*.json', dir: target, how, borrowed };
246
+ if (dryRun) return { status: 'would-register', file: 'a new local_*.json', dir: target, how, label, borrowed };
183
247
 
184
248
  const entry = buildDesktopEntry({ template, cliSessionId: id, root, transcript: parseJsonl(fs.readFileSync(mainFile, 'utf8')) });
185
249
  const file = `${entry.sessionId}.json`;
@@ -187,7 +251,7 @@ export function registerDesktopSession({ id, root, mainFile, home, dir, platform
187
251
  const tmp = `${dest}.carry-tmp`;
188
252
  fs.writeFileSync(tmp, JSON.stringify(entry), { mode: 0o600 });
189
253
  fs.renameSync(tmp, dest);
190
- return { status: 'registered', file, dir: target, how, borrowed };
254
+ return { status: 'registered', file, dir: target, how, label, borrowed };
191
255
  }
192
256
 
193
257
  /** cliSessionId -> { title, archived } as the desktop app lists it. Empty when there is no desktop app data. */