supertelegram 0.3.2 → 0.5.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.
@@ -0,0 +1,52 @@
1
+ name: publish
2
+
3
+ # publishes to npm when you push a version tag (e.g. v0.5.0) or cut a GitHub release.
4
+ # tag the current version with: git tag v$(node -p "require('./package.json').version") && git push --tags
5
+ # auth is via npm OIDC trusted publishing (no NPM_TOKEN) — configure a trusted
6
+ # publisher for this package on npmjs.com pointing at this repo + workflow.
7
+
8
+ on:
9
+ push:
10
+ tags:
11
+ - "v*"
12
+ release:
13
+ types: [published]
14
+ workflow_dispatch:
15
+
16
+ jobs:
17
+ publish:
18
+ runs-on: ubuntu-latest
19
+ permissions:
20
+ contents: read
21
+ id-token: write # enables npm provenance
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - uses: oven-sh/setup-bun@v2
26
+ with:
27
+ bun-version: latest
28
+
29
+ - name: install deps
30
+ run: bun install --frozen-lockfile
31
+
32
+ - uses: actions/setup-node@v4
33
+ with:
34
+ node-version: 24
35
+ registry-url: "https://registry.npmjs.org"
36
+
37
+ # OIDC trusted publishing needs a recent npm (node 24 ships one, but pin latest)
38
+ - name: upgrade npm
39
+ run: npm install -g npm@latest
40
+
41
+ - name: guard — tag matches package.json version
42
+ if: startsWith(github.ref, 'refs/tags/')
43
+ run: |
44
+ PKG_VERSION="v$(node -p "require('./package.json').version")"
45
+ if [ "$PKG_VERSION" != "${GITHUB_REF_NAME}" ]; then
46
+ echo "::error::tag ${GITHUB_REF_NAME} does not match package.json ($PKG_VERSION)"
47
+ exit 1
48
+ fi
49
+
50
+ # no NODE_AUTH_TOKEN: npm authenticates via OIDC (id-token: write above)
51
+ - name: publish
52
+ run: npm publish --access public --provenance
package/PUBLISHING.md CHANGED
@@ -1,6 +1,23 @@
1
1
  # publishing guide
2
2
 
3
- ## setup
3
+ ## automated (github actions) — preferred
4
+
5
+ CI publishes to npm on every version tag. auth is via **OIDC trusted
6
+ publishing** — no `NPM_TOKEN` secret. one-time setup: on npmjs.com, open the
7
+ package → Settings → Trusted Publishing, and add a GitHub Actions publisher
8
+ pointing at `caffeinum/supertelegram`, workflow `publish.yml`.
9
+
10
+ then to release:
11
+ ```bash
12
+ # bump version in package.json first, commit, then:
13
+ git tag "v$(node -p "require('./package.json').version")"
14
+ git push --tags
15
+ ```
16
+ the `.github/workflows/publish.yml` workflow checks the tag matches
17
+ package.json, installs, and runs `npm publish --access public --provenance`.
18
+ you can also trigger it manually from the Actions tab (workflow_dispatch).
19
+
20
+ ## manual setup
4
21
 
5
22
  1. login to npm:
6
23
  ```bash
package/README.md CHANGED
@@ -32,9 +32,11 @@ that's it! now you're ready to use telegram from the cli.
32
32
  ### messages
33
33
 
34
34
  ```bash
35
- # send a message
35
+ # send a message — chat can be @username, a numeric id, or a name
36
36
  telegram send @username "hello there"
37
- telegram send "me" "note to self" # use "me" for saved messages
37
+ telegram send 7069934904 "hey" # by id (for chats with no username)
38
+ telegram send "Ben YC" "hello" # by name (partial, case-insensitive)
39
+ telegram send "me" "note to self" # use "me" for saved messages
38
40
 
39
41
  # read messages (shows [photo], [video], [file] indicators)
40
42
  telegram read @username 10
@@ -45,7 +47,7 @@ telegram reply "John" "hey back!"
45
47
  # get unread messages (json output with media info)
46
48
  telegram unread 20
47
49
 
48
- # list dialogs
50
+ # list dialogs (shows @username and [id: ...] for each chat)
49
51
  telegram dialogs 10
50
52
  ```
51
53
 
@@ -76,15 +78,42 @@ telegram config set appHash "abc123..."
76
78
  - `--help` - show help
77
79
  - `--version` - show version
78
80
 
81
+ ### multiple accounts
82
+
83
+ log in to as many accounts as you want, each stored under a name, and switch
84
+ between them:
85
+
86
+ ```bash
87
+ # log into named accounts (prompts phone/code the first time)
88
+ telegram login personal
89
+ telegram login work
90
+
91
+ # see them (* marks the active one)
92
+ telegram accounts
93
+ # * work — @yourworkhandle
94
+ # personal — @yourhandle
95
+
96
+ # switch the active account (all later commands use it)
97
+ telegram switch personal
98
+ telegram whoami # personal — @yourhandle
99
+
100
+ # or run a single command as another account without switching
101
+ telegram -a work send @boss "on it"
102
+
103
+ # remove an account
104
+ telegram logout work
105
+ ```
106
+
107
+ sessions live in `~/.supertelegram/accounts/<name>.txt`; the active account is
108
+ tracked in `~/.supertelegram/accounts.json`. API credentials (appId/appHash)
109
+ are shared across accounts. upgrading from an older version? your existing
110
+ login is migrated automatically into an account named `default`.
111
+
79
112
  ### advanced
80
113
 
81
- **multi-account / custom session location:**
114
+ **custom session location (one-off / scripting):**
82
115
  ```bash
83
- # use env var
84
116
  TELEGRAM_SESSION=./custom.txt telegram send @friend "hey"
85
-
86
- # or pass flag (todo)
87
- telegram send @friend "hey" --session ./custom.txt
88
117
  ```
89
118
 
90
119
  **precedence for API credentials:**
@@ -93,9 +122,11 @@ telegram send @friend "hey" --session ./custom.txt
93
122
  3. `.env` file in current directory (for dev)
94
123
 
95
124
  **precedence for session file:**
96
- 1. `TELEGRAM_SESSION` env var
97
- 2. `~/.supertelegram/session.txt` (global default)
98
- 3. `./session.txt` (backwards compat)
125
+ 1. `--account <name>` flag
126
+ 2. `TELEGRAM_SESSION` env var
127
+ 3. active account (`~/.supertelegram/accounts/<name>.txt`)
128
+ 4. `./session.txt` (backwards compat)
129
+ 5. `~/.supertelegram/session.txt` (legacy default)
99
130
 
100
131
  ## development
101
132
 
package/package.json CHANGED
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "name": "supertelegram",
3
- "version": "0.3.2",
3
+ "version": "0.5.0",
4
4
  "description": "telegram cli for humans and bots",
5
5
  "module": "index.ts",
6
6
  "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/caffeinum/supertelegram.git"
10
+ },
11
+ "homepage": "https://github.com/caffeinum/supertelegram#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/caffeinum/supertelegram/issues"
14
+ },
7
15
  "bin": {
8
16
  "telegram": "src/cli/run.ts"
9
17
  },
@@ -11,6 +11,15 @@ import {
11
11
  } from "../client/telegram";
12
12
  import { askPhoneNumber, askPhoneCode, askPassword, askAppId, askAppHash } from "./prompts";
13
13
  import { getApiCredentials, setConfig } from "../config/manager";
14
+ import {
15
+ listAccounts,
16
+ setCurrentAccount,
17
+ registerAccount,
18
+ removeAccount,
19
+ getCurrentAccount,
20
+ accountSessionPath,
21
+ } from "../config/accounts";
22
+ import { setSessionPath } from "../client/telegram";
14
23
  import { Api } from "telegram";
15
24
 
16
25
  export async function send(username: string, message: string) {
@@ -71,8 +80,16 @@ export async function dialogs(limit = 10) {
71
80
 
72
81
  const dialogList = await getDialogs(limit);
73
82
  for (const dialog of dialogList) {
74
- console.log(`- ${dialog.title}`);
83
+ const entity = dialog.entity;
84
+ const username =
85
+ entity && "username" in entity && entity.username
86
+ ? ` @${entity.username}`
87
+ : "";
88
+ const id = dialog.id?.toString() ?? entity?.id?.toString() ?? "?";
89
+ const unread = dialog.unreadCount > 0 ? ` (${dialog.unreadCount} unread)` : "";
90
+ console.log(`- ${dialog.title}${username} [id: ${id}]${unread}`);
75
91
  }
92
+ console.log("\nsend with: telegram send <@username|id> \"your message\"");
76
93
  await disconnect();
77
94
  }
78
95
 
@@ -152,37 +169,118 @@ export async function reply(chatName: string, message: string) {
152
169
  await disconnect();
153
170
  }
154
171
 
155
- export async function login() {
156
- // check if API credentials are configured
172
+ export async function login(accountName?: string) {
173
+ const name = accountName || getCurrentAccount() || "default";
174
+ // isolate this login to the named account's own session file
175
+ setSessionPath(accountSessionPath(name));
176
+
177
+ // check if API credentials are configured (shared across accounts)
157
178
  const creds = getApiCredentials();
158
179
  if (!creds) {
159
180
  console.log("no API credentials found. let's set them up first.");
160
181
  const appId = await askAppId();
161
182
  const appHash = await askAppHash();
162
-
183
+
163
184
  setConfig("appId", appId);
164
185
  setConfig("appHash", appHash);
165
186
  console.log("credentials saved to ~/.supertelegram/config.json\n");
166
187
  }
167
188
 
168
189
  const loggedIn = await isLoggedIn();
169
- if (loggedIn) {
170
- console.log("already logged in!");
171
- await disconnect();
172
- return;
190
+ if (!loggedIn) {
191
+ console.log(`starting login for account "${name}"...`);
192
+ await telegramLogin({
193
+ phoneNumber: askPhoneNumber,
194
+ phoneCode: askPhoneCode,
195
+ password: askPassword,
196
+ });
197
+ } else {
198
+ console.log(`account "${name}" already has a session, refreshing details...`);
173
199
  }
174
200
 
175
- console.log("starting login...");
176
- await telegramLogin({
177
- phoneNumber: askPhoneNumber,
178
- phoneCode: askPhoneCode,
179
- password: askPassword,
201
+ // capture identity + register (and make current)
202
+ const client = await getClient();
203
+ const me = await client.getMe();
204
+ const fullName = [me.firstName, me.lastName].filter(Boolean).join(" ");
205
+ registerAccount(name, {
206
+ username: me.username ?? undefined,
207
+ userId: me.id?.toString(),
208
+ name: fullName || undefined,
180
209
  });
181
210
 
182
- console.log("login complete!");
211
+ const label = me.username ? `@${me.username}` : fullName || name;
212
+ console.log(`logged in as ${label} — account "${name}" is now active`);
183
213
  await disconnect();
184
214
  }
185
215
 
216
+ export async function accounts() {
217
+ const list = listAccounts();
218
+ if (list.length === 0) {
219
+ console.log("no accounts yet. run: telegram login <name>");
220
+ return;
221
+ }
222
+ for (const { name, meta, current } of list) {
223
+ const marker = current ? "*" : " ";
224
+ const who = meta.username
225
+ ? `@${meta.username}`
226
+ : meta.name || (meta.userId ? `id ${meta.userId}` : "");
227
+ console.log(`${marker} ${name}${who ? ` — ${who}` : ""}`);
228
+ }
229
+ console.log("\nswitch with: telegram switch <name>");
230
+ }
231
+
232
+ export async function switchAccount(name?: string) {
233
+ if (!name) {
234
+ console.error("usage: telegram switch <name>");
235
+ console.error("see accounts with: telegram accounts");
236
+ process.exit(1);
237
+ }
238
+ setCurrentAccount(name);
239
+ console.log(`switched to account "${name}"`);
240
+ }
241
+
242
+ export async function logout(name?: string) {
243
+ const target = name || getCurrentAccount();
244
+ if (!target) {
245
+ console.error("no account to log out. see: telegram accounts");
246
+ process.exit(1);
247
+ }
248
+ removeAccount(target);
249
+ const now = getCurrentAccount();
250
+ console.log(
251
+ `logged out of "${target}"` + (now ? `. active account is now "${now}"` : ". no accounts left")
252
+ );
253
+ }
254
+
255
+ export async function whoami() {
256
+ const current = getCurrentAccount();
257
+ if (!current) {
258
+ console.log("not logged in. run: telegram login");
259
+ return;
260
+ }
261
+
262
+ let meta = listAccounts().find((a) => a.name === current)?.meta;
263
+
264
+ // backfill identity for accounts migrated without metadata
265
+ if (meta && !meta.username && !meta.name && (await isLoggedIn())) {
266
+ const client = await getClient();
267
+ const me = await client.getMe();
268
+ const fullName = [me.firstName, me.lastName].filter(Boolean).join(" ");
269
+ meta = {
270
+ username: me.username ?? undefined,
271
+ userId: me.id?.toString(),
272
+ name: fullName || undefined,
273
+ };
274
+ registerAccount(current, meta, false);
275
+ await disconnect();
276
+ }
277
+
278
+ const who = meta?.username
279
+ ? `@${meta.username}`
280
+ : meta?.name || (meta?.userId ? `id ${meta.userId}` : "unknown");
281
+ console.log(`${current} — ${who}`);
282
+ }
283
+
186
284
  export async function config(action?: string, key?: string, value?: string) {
187
285
  if (action === "set" && key && value) {
188
286
  setConfig(key, value);
package/src/cli/run.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env bun
2
- import { send, read, dialogs, unread, reply, login, config, sendFile, downloadMedia } from "./commands";
2
+ import { send, read, dialogs, unread, reply, login, config, sendFile, downloadMedia, accounts, switchAccount, logout, whoami } from "./commands";
3
3
  import { setVerbose, setSessionPath } from "../client/telegram";
4
+ import { migrateLegacyIfNeeded, accountSessionPath } from "../config/accounts";
5
+ import pkg from "../../package.json";
4
6
 
5
- const VERSION = "0.1.0";
7
+ const VERSION = pkg.version;
6
8
  const NAME = "telegram";
7
9
 
8
10
  const HELP = `
@@ -12,17 +14,22 @@ usage:
12
14
  ${NAME} <command> [options]
13
15
 
14
16
  commands:
15
- send <chat> <message> send a message to a chat
17
+ send <chat> <message> send a message (chat = @username, id, or name)
16
18
  send-file <chat> <path> send a file/image/video (optional: caption)
17
19
  read <chat> [limit] read messages from a chat (default: 10)
18
20
  download <chat> <id> [out] download media from message id
19
21
  reply <chat> <message> reply to a chat by name (partial match)
20
22
  dialogs [limit] list recent dialogs (default: 10)
21
23
  unread [limit] show unread messages as json (default: 20)
22
- login authenticate with telegram
24
+ login [name] authenticate with telegram (into a named account)
25
+ accounts list logged-in accounts (* = current)
26
+ switch <name> switch the active account
27
+ whoami show the active account
28
+ logout [name] remove an account (default: current)
23
29
  config set <key> <val> set API credentials (appId, appHash)
24
30
 
25
31
  options:
32
+ -a, --account <name> run this command as a specific account
26
33
  -v, --verbose show debug logs
27
34
  -h, --help show this help
28
35
  --version show version
@@ -35,6 +42,9 @@ examples:
35
42
  ${NAME} reply "John" "hey!"
36
43
  ${NAME} unread
37
44
  ${NAME} dialogs 20
45
+ ${NAME} login work # log into a second account named "work"
46
+ ${NAME} switch work # make it active
47
+ ${NAME} -a personal unread # run one command as another account
38
48
  `.trim();
39
49
 
40
50
  const rawArgs = process.argv.slice(2);
@@ -51,6 +61,15 @@ if (rawArgs.includes("--version")) {
51
61
  }
52
62
 
53
63
  const verbose = rawArgs.includes("--verbose") || rawArgs.includes("-v");
64
+
65
+ // pull out --account/-a <name> before positional parsing
66
+ let accountFlag: string | undefined;
67
+ const accIdx = rawArgs.findIndex((a) => a === "--account" || a === "-a");
68
+ if (accIdx !== -1) {
69
+ accountFlag = rawArgs[accIdx + 1];
70
+ rawArgs.splice(accIdx, accountFlag ? 2 : 1);
71
+ }
72
+
54
73
  const args = rawArgs.filter((a) => !a.startsWith("-"));
55
74
  const [command, ...rest] = args;
56
75
 
@@ -58,6 +77,14 @@ if (verbose) {
58
77
  setVerbose(true);
59
78
  }
60
79
 
80
+ // fold a pre-multi-account session.txt into account "default" (one-time, no-op after)
81
+ migrateLegacyIfNeeded();
82
+
83
+ // an explicit --account pins this invocation to that account's session
84
+ if (accountFlag) {
85
+ setSessionPath(accountSessionPath(accountFlag));
86
+ }
87
+
61
88
  async function main() {
62
89
  switch (command) {
63
90
  case "send":
@@ -109,7 +136,23 @@ async function main() {
109
136
  break;
110
137
 
111
138
  case "login":
112
- await login();
139
+ await login(rest[0]);
140
+ break;
141
+
142
+ case "accounts":
143
+ await accounts();
144
+ break;
145
+
146
+ case "switch":
147
+ await switchAccount(rest[0]);
148
+ break;
149
+
150
+ case "whoami":
151
+ await whoami();
152
+ break;
153
+
154
+ case "logout":
155
+ await logout(rest[0]);
113
156
  break;
114
157
 
115
158
  case "config":
@@ -76,12 +76,60 @@ export async function login(callbacks: {
76
76
  console.log("session saved!");
77
77
  }
78
78
 
79
+ // a chat can be addressed by @username, "me", phone (+123), t.me link,
80
+ // or a numeric id copied from `telegram dialogs`. usernames/links resolve
81
+ // natively; numeric ids and bare names need the dialog entity cache, so we
82
+ // look them up here.
83
+ export async function resolveEntity(
84
+ identifier: string
85
+ ): Promise<string | Api.TypeEntityLike> {
86
+ const c = await getClient();
87
+ const id = identifier.trim();
88
+
89
+ // things gramjs resolves on its own
90
+ if (
91
+ id === "me" ||
92
+ id.startsWith("@") ||
93
+ id.startsWith("+") ||
94
+ id.startsWith("http")
95
+ ) {
96
+ return id;
97
+ }
98
+
99
+ // numeric id (users positive, chats/channels may be negative)
100
+ if (/^-?\d+$/.test(id)) {
101
+ try {
102
+ // fast path: entity already in the session cache
103
+ return await c.getInputEntity(id);
104
+ } catch {
105
+ // slow path: fetch dialogs to populate + match the entity
106
+ const match = (await c.getDialogs({ limit: 200 })).find(
107
+ (d) => d.id?.toString() === id || d.entity?.id?.toString() === id
108
+ );
109
+ if (match?.entity) return match.entity;
110
+ throw new Error(
111
+ `chat with id ${id} not found in your dialogs. run: telegram dialogs 200`
112
+ );
113
+ }
114
+ }
115
+
116
+ // bare name: partial, case-insensitive match against dialog titles
117
+ const match = (await c.getDialogs({ limit: 200 })).find((d) =>
118
+ d.title?.toLowerCase().includes(id.toLowerCase())
119
+ );
120
+ if (match?.entity) return match.entity;
121
+
122
+ // let gramjs take a final shot (e.g. a username without the @)
123
+ return id;
124
+ }
125
+
79
126
  export async function sendMessage(
80
127
  username: string,
81
128
  message: string
82
129
  ): Promise<Api.Message> {
83
130
  const c = await getClient();
84
- return c.sendMessage(username, { message });
131
+ const entity = await resolveEntity(username);
132
+ return c.sendMessage(entity, { message });
85
133
  }
86
134
 
87
135
  export async function sendFile(
@@ -90,7 +138,8 @@ export async function sendFile(
90
138
  caption?: string
91
139
  ): Promise<Api.Message> {
92
140
  const c = await getClient();
93
- return c.sendFile(username, {
141
+ const entity = await resolveEntity(username);
142
+ return c.sendFile(entity, {
94
143
  file: filePath,
95
144
  caption: caption,
96
145
  });
@@ -117,7 +166,8 @@ export async function getMessages(
117
166
  limit = 10
118
167
  ): Promise<Api.Message[]> {
119
168
  const c = await getClient();
120
- const messages = await c.getMessages(username, { limit });
169
+ const entity = await resolveEntity(username);
170
+ const messages = await c.getMessages(entity, { limit });
121
171
  return messages as Api.Message[];
122
172
  }
123
173
 
@@ -0,0 +1,113 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ writeFileSync,
6
+ copyFileSync,
7
+ rmSync,
8
+ } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+
12
+ // kept in sync with manager.ts; recomputed here to avoid a circular import
13
+ const CONFIG_DIR = join(homedir(), ".supertelegram");
14
+ const ACCOUNTS_DIR = join(CONFIG_DIR, "accounts");
15
+ const ACCOUNTS_FILE = join(CONFIG_DIR, "accounts.json");
16
+ const LEGACY_SESSION_FILE = join(CONFIG_DIR, "session.txt");
17
+
18
+ export interface AccountMeta {
19
+ username?: string;
20
+ userId?: string;
21
+ name?: string;
22
+ }
23
+
24
+ export interface AccountsRegistry {
25
+ current?: string;
26
+ accounts: Record<string, AccountMeta>;
27
+ }
28
+
29
+ function readRegistry(): AccountsRegistry {
30
+ if (!existsSync(ACCOUNTS_FILE)) return { accounts: {} };
31
+ try {
32
+ return JSON.parse(readFileSync(ACCOUNTS_FILE, "utf-8"));
33
+ } catch {
34
+ return { accounts: {} };
35
+ }
36
+ }
37
+
38
+ function writeRegistry(reg: AccountsRegistry): void {
39
+ if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
40
+ writeFileSync(ACCOUNTS_FILE, JSON.stringify(reg, null, 2));
41
+ }
42
+
43
+ export function accountSessionPath(name: string): string {
44
+ return join(ACCOUNTS_DIR, `${name}.txt`);
45
+ }
46
+
47
+ export function getCurrentAccount(): string | undefined {
48
+ return readRegistry().current;
49
+ }
50
+
51
+ export function listAccounts(): {
52
+ name: string;
53
+ meta: AccountMeta;
54
+ current: boolean;
55
+ }[] {
56
+ const reg = readRegistry();
57
+ return Object.entries(reg.accounts).map(([name, meta]) => ({
58
+ name,
59
+ meta,
60
+ current: name === reg.current,
61
+ }));
62
+ }
63
+
64
+ export function setCurrentAccount(name: string): void {
65
+ const reg = readRegistry();
66
+ if (!reg.accounts[name]) {
67
+ const known = Object.keys(reg.accounts);
68
+ throw new Error(
69
+ `account "${name}" not found.` +
70
+ (known.length ? ` known accounts: ${known.join(", ")}` : " run: telegram login <name>")
71
+ );
72
+ }
73
+ reg.current = name;
74
+ writeRegistry(reg);
75
+ }
76
+
77
+ export function registerAccount(
78
+ name: string,
79
+ meta: AccountMeta,
80
+ makeCurrent = true
81
+ ): void {
82
+ if (!existsSync(ACCOUNTS_DIR)) mkdirSync(ACCOUNTS_DIR, { recursive: true });
83
+ const reg = readRegistry();
84
+ reg.accounts[name] = meta;
85
+ if (makeCurrent) reg.current = name;
86
+ writeRegistry(reg);
87
+ }
88
+
89
+ export function removeAccount(name: string): void {
90
+ const reg = readRegistry();
91
+ if (!reg.accounts[name]) {
92
+ throw new Error(`account "${name}" not found`);
93
+ }
94
+ delete reg.accounts[name];
95
+ if (reg.current === name) reg.current = Object.keys(reg.accounts)[0];
96
+ writeRegistry(reg);
97
+
98
+ const sessionFile = accountSessionPath(name);
99
+ if (existsSync(sessionFile)) rmSync(sessionFile);
100
+ }
101
+
102
+ // one-time migration: fold a pre-multi-account session.txt into account "default"
103
+ // so existing installs keep their login after upgrading.
104
+ export function migrateLegacyIfNeeded(): void {
105
+ const reg = readRegistry();
106
+ if (Object.keys(reg.accounts).length > 0) return; // already on the accounts system
107
+
108
+ if (existsSync(LEGACY_SESSION_FILE) && readFileSync(LEGACY_SESSION_FILE, "utf-8").trim()) {
109
+ if (!existsSync(ACCOUNTS_DIR)) mkdirSync(ACCOUNTS_DIR, { recursive: true });
110
+ copyFileSync(LEGACY_SESSION_FILE, accountSessionPath("default"));
111
+ registerAccount("default", {}, true);
112
+ }
113
+ }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import { getCurrentAccount, accountSessionPath } from "./accounts";
4
5
 
5
6
  const CONFIG_DIR = join(homedir(), ".supertelegram");
6
7
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
@@ -44,24 +45,30 @@ export function setConfig(key: string, value: string) {
44
45
 
45
46
  export function getSessionPath(customPath?: string): string {
46
47
  // precedence:
47
- // 1. custom path from flag
48
+ // 1. custom path from flag (--account resolves to one of these)
48
49
  // 2. TELEGRAM_SESSION env var
49
- // 3. global ~/.supertelegram/session.txt
50
+ // 3. current account's session (~/.supertelegram/accounts/<name>.txt)
50
51
  // 4. local ./session.txt (backwards compat)
51
-
52
+ // 5. global ~/.supertelegram/session.txt (legacy default)
53
+
52
54
  if (customPath) {
53
55
  return customPath;
54
56
  }
55
-
57
+
56
58
  if (process.env.TELEGRAM_SESSION) {
57
59
  return process.env.TELEGRAM_SESSION;
58
60
  }
59
-
61
+
62
+ const current = getCurrentAccount();
63
+ if (current) {
64
+ return accountSessionPath(current);
65
+ }
66
+
60
67
  // check if local session.txt exists (backwards compat)
61
68
  if (existsSync("./session.txt")) {
62
69
  return "./session.txt";
63
70
  }
64
-
71
+
65
72
  return SESSION_FILE;
66
73
  }
67
74
 
package/tsconfig.json CHANGED
@@ -11,6 +11,7 @@
11
11
  // Bundler mode
12
12
  "moduleResolution": "bundler",
13
13
  "allowImportingTsExtensions": true,
14
+ "resolveJsonModule": true,
14
15
  "verbatimModuleSyntax": true,
15
16
  "noEmit": true,
16
17