@yanzc/claude-pro 0.4.1 → 0.4.3
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/package.json +40 -40
- package/src/api.js +23 -9
- package/src/codex/client.js +3 -0
- package/src/commands.js +8 -8
- package/src/gateway.js +17 -2
- package/src/tools/peers.js +3 -1
- package/src/ui/repl.js +10 -11
package/package.json
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@yanzc/claude-pro",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "claude-pro: a self-implemented coding agent for Anthropic-compatible APIs or local Claude Code subscription login. Full access with no permission prompts; file edits are checkpointed and can be rewound.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"repository": {
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/yanzaocheng/copy-claude-code.git"
|
|
9
|
-
},
|
|
10
|
-
"homepage": "https://github.com/yanzaocheng/copy-claude-code#readme",
|
|
11
|
-
"keywords": ["claude", "coding-agent", "cli", "anthropic", "ai"],
|
|
12
|
-
"bin": {
|
|
13
|
-
"claude-pro": "bin/claude-pro.js"
|
|
14
|
-
},
|
|
15
|
-
"files": [
|
|
16
|
-
"bin/",
|
|
17
|
-
"src/",
|
|
18
|
-
".env.example",
|
|
19
|
-
"README.md",
|
|
20
|
-
"LICENSE"
|
|
21
|
-
],
|
|
22
|
-
"scripts": {
|
|
23
|
-
"start": "node bin/claude-pro.js",
|
|
24
|
-
"check": "node bin/claude-pro.js --self-check",
|
|
25
|
-
"test": "node --test --test-force-exit --test-timeout=60000 --test-reporter=dot \"test/**/*.test.js\""
|
|
26
|
-
},
|
|
27
|
-
"engines": {
|
|
28
|
-
"node": ">=22"
|
|
29
|
-
},
|
|
30
|
-
"license": "MIT",
|
|
31
|
-
"publishConfig": {
|
|
32
|
-
"access": "public"
|
|
33
|
-
},
|
|
34
|
-
"dependencies": {
|
|
35
|
-
"@anthropic-ai/sdk": "^0.128.0"
|
|
36
|
-
},
|
|
37
|
-
"devDependencies": {
|
|
38
|
-
"@xterm/headless": "^6.0.0"
|
|
39
|
-
}
|
|
40
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@yanzc/claude-pro",
|
|
3
|
+
"version": "0.4.3",
|
|
4
|
+
"description": "claude-pro: a self-implemented coding agent for Anthropic-compatible APIs or local Claude Code subscription login. Full access with no permission prompts; file edits are checkpointed and can be rewound.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/yanzaocheng/copy-claude-code.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/yanzaocheng/copy-claude-code#readme",
|
|
11
|
+
"keywords": ["claude", "coding-agent", "cli", "anthropic", "ai"],
|
|
12
|
+
"bin": {
|
|
13
|
+
"claude-pro": "bin/claude-pro.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin/",
|
|
17
|
+
"src/",
|
|
18
|
+
".env.example",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"start": "node bin/claude-pro.js",
|
|
24
|
+
"check": "node bin/claude-pro.js --self-check",
|
|
25
|
+
"test": "node --test --test-force-exit --test-timeout=60000 --test-reporter=dot \"test/**/*.test.js\""
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=22"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@anthropic-ai/sdk": "^0.128.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@xterm/headless": "^6.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/api.js
CHANGED
|
@@ -7,7 +7,7 @@ import { refreshClaudeCodeLogin, canRenewLogin } from './oauth.js';
|
|
|
7
7
|
import { humanDuration, parseRateLimit, parseRetryAfter, planRetry, sleepUntil, timing } from './ratelimit.js';
|
|
8
8
|
import { cacheControl, cachePlanOf, fingerprint } from './cache.js';
|
|
9
9
|
import { readGatewayLogin, fetchGatewayModels, cachedGatewayModel, gatewayEffort, gatewayDefaultEffort } from './gateway.js';
|
|
10
|
-
import { createCodexFetch, codexModels } from './codex/client.js';
|
|
10
|
+
import { createCodexFetch, codexModels, codexEffort, nearestEffort } from './codex/client.js';
|
|
11
11
|
import { SIGNATURE_PREFIX } from './codex/translate-request.js';
|
|
12
12
|
|
|
13
13
|
let cached = null;
|
|
@@ -197,21 +197,35 @@ export const effortSupported = (modelId) => !noEffort.has(modelId);
|
|
|
197
197
|
// The effort a model uses when none is sent (docs.claude.com, "Effort"): medium on Opus 5.5 and Haiku 5.5, high on
|
|
198
198
|
// the other models that take one; for a GPT model, the gateway's metadata or the local Codex models cache. null for
|
|
199
199
|
// a model that takes none or is not known.
|
|
200
|
+
// The environment that says where the Codex install is: CODEX_HOME may be set in the shell only.
|
|
201
|
+
const codexEnv = (config) => ({ ...process.env, ...(config?.env || {}) });
|
|
202
|
+
const gatewayModelInfo = (config, id) => (config?.usesGateway ? cachedGatewayModel(readGatewayLogin(config.gatewayUrl), id) : null);
|
|
203
|
+
|
|
200
204
|
export function defaultEffort(modelId, config = null) {
|
|
201
205
|
const id = String(modelId || '');
|
|
202
206
|
if (!effortSupported(id)) return null;
|
|
203
207
|
if (/^gpt-/i.test(id)) {
|
|
204
|
-
|
|
205
|
-
const login = readGatewayLogin(config.gatewayUrl);
|
|
206
|
-
return gatewayDefaultEffort(cachedGatewayModel(login, id)) || codexModels(config.env).models.find((m) => m.slug === id)?.defaultEffort || null;
|
|
207
|
-
}
|
|
208
|
-
return codexModels(config?.env).models.find((m) => m.slug === id)?.defaultEffort || null;
|
|
208
|
+
return gatewayDefaultEffort(gatewayModelInfo(config, id)) || codexModels(codexEnv(config)).models.find((m) => m.slug === id)?.defaultEffort || null;
|
|
209
209
|
}
|
|
210
210
|
if (/^claude-(?:opus|haiku)-5-5(?:-|$)/i.test(id)) return 'medium';
|
|
211
211
|
if (/^claude-(?:fable|mythos|opus-(?:5|4-[5-8])|sonnet-(?:5|4-6))(?:-|$)/i.test(id)) return 'high';
|
|
212
212
|
return null;
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
// The effort levels Claude takes, as Claude Code's /effort offers them. OpenAI's none and minimal and the ultra of
|
|
216
|
+
// some GPT models are not among them; a level chosen on a GPT model stays chosen after a switch to Claude.
|
|
217
|
+
export const CLAUDE_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
218
|
+
|
|
219
|
+
// The level a request for this model carries when `effort` is chosen: the nearest one the model takes. A GPT model
|
|
220
|
+
// lists its own (the gateway's /v1/models, else the local Codex models cache); Claude takes CLAUDE_EFFORTS.
|
|
221
|
+
export function effortToSend(config, modelId, effort) {
|
|
222
|
+
if (!effort) return effort;
|
|
223
|
+
if (/^claude-/i.test(modelId)) return nearestEffort(effort, CLAUDE_EFFORTS);
|
|
224
|
+
if (!/^gpt-/i.test(modelId)) return effort === 'ultra' ? 'max' : effort;
|
|
225
|
+
const info = gatewayModelInfo(config, modelId);
|
|
226
|
+
return info ? gatewayEffort(info, effort) : codexEffort(modelId, effort, codexEnv(config));
|
|
227
|
+
}
|
|
228
|
+
|
|
215
229
|
// Pull the allowed maximum out of an error such as
|
|
216
230
|
// "max_tokens: 128000 > 64000, which is the maximum allowed number of output tokens for claude-..."
|
|
217
231
|
// "Invalid max_tokens value, the valid range of max_tokens is [1, 8192]"
|
|
@@ -277,8 +291,7 @@ export function buildParams(config, { model, system, messages, tools, maxTokens,
|
|
|
277
291
|
}
|
|
278
292
|
// A GPT model with no effort set gets its default from the gateway's model metadata (or the local Codex cache
|
|
279
293
|
// when using the Codex provider), sent explicitly so that the footer and backend use the same level.
|
|
280
|
-
|
|
281
|
-
if (config.usesGateway && /^gpt-/i.test(model.id)) eff = gatewayEffort(cachedGatewayModel(readGatewayLogin(config.gatewayUrl), model.id), eff);
|
|
294
|
+
const eff = effortToSend(config, model.id, (effort === undefined ? config.effort : effort) || (/^gpt-/i.test(model.id) ? defaultEffort(model.id, config) : null));
|
|
282
295
|
if (eff && !noEffort.has(model.id)) params.output_config = { effort: eff };
|
|
283
296
|
const budget = config.thinkingBudget;
|
|
284
297
|
if (budget === 0) params.thinking = { type: 'disabled' };
|
|
@@ -344,7 +357,8 @@ export async function streamMessage(config, request, { signal, onEvent = () => {
|
|
|
344
357
|
if (config.usesGateway && /^gpt-/i.test(request.model?.id)) {
|
|
345
358
|
const gatewayLogin = readGatewayLogin(config.gatewayUrl);
|
|
346
359
|
if (gatewayLogin) {
|
|
347
|
-
|
|
360
|
+
// A gateway that does not answer costs at most a few seconds once (see fetchGatewayModels), not the request.
|
|
361
|
+
try { await fetchGatewayModels(gatewayLogin, { signal }); } catch { if (signal?.aborted) throw Object.assign(new Error('aborted'), { name: 'AbortError', partial: [] }); }
|
|
348
362
|
}
|
|
349
363
|
}
|
|
350
364
|
let client = getClient(config, login);
|
package/src/codex/client.js
CHANGED
|
@@ -174,6 +174,9 @@ function clampEffort(effort, supported) {
|
|
|
174
174
|
return supported.reduce((best, s) => (Math.abs(EFFORTS.indexOf(s) - want) < Math.abs(EFFORTS.indexOf(best) - want) ? s : best), supported[0]);
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
// The level of `supported` nearest to `effort` (on the scale none … ultra); `effort` itself when it is supported.
|
|
178
|
+
export const nearestEffort = (effort, supported) => clampEffort(effort, supported);
|
|
179
|
+
|
|
177
180
|
// The effort levels a GPT model takes, from the local Codex models cache ([] when it is not listed there):
|
|
178
181
|
// gpt-6.1-sol takes low … ultra, but not none or minimal.
|
|
179
182
|
export const codexEfforts = (modelId, env = process.env) => codexModels(env).models.find((m) => m.slug === modelId)?.efforts || [];
|
package/src/commands.js
CHANGED
|
@@ -6,8 +6,8 @@ import { c, padEnd, truncate } from './ui/ansi.js';
|
|
|
6
6
|
import { SelectDialog } from './ui/dialogs.js';
|
|
7
7
|
import { renderTodos, resultLines, formatTokens, formatDuration } from './ui/render.js';
|
|
8
8
|
import { loadConfig, settingsFiles, updateSettingsFile, rememberChoice, applyRememberedChoices, KNOWN_KEYS, VERSION, APP_NAME, CONFIG_DIR } from './config.js';
|
|
9
|
-
import { loginWithBrowser, readGatewayLogin, clearGatewayLogin, fetchGatewayAccount, fetchGatewayModels, cachedGatewayModel, gatewayEfforts,
|
|
10
|
-
import { codexLoginSummary, codexModels, codexEfforts
|
|
9
|
+
import { loginWithBrowser, readGatewayLogin, clearGatewayLogin, fetchGatewayAccount, fetchGatewayModels, cachedGatewayModel, gatewayEfforts, formatUsd } from './gateway.js';
|
|
10
|
+
import { codexLoginSummary, codexModels, codexEfforts } from './codex/client.js';
|
|
11
11
|
import { readClaudeCodeLogin } from './claude-code.js';
|
|
12
12
|
import { SessionStore } from './session.js';
|
|
13
13
|
import { memoryFiles, listAutoMemories } from './context.js';
|
|
@@ -15,7 +15,7 @@ import { INIT_PROMPT } from './prompts.js';
|
|
|
15
15
|
import { loadCustomCommands, renderSkill, expandCommandBody, expandShellEmbeds } from './agents.js';
|
|
16
16
|
import { listBackgroundShells } from './tools/bash.js';
|
|
17
17
|
import { estimateTokens } from './agent.js';
|
|
18
|
-
import { quickQuery, cacheRefusals, defaultEffort } from './api.js';
|
|
18
|
+
import { quickQuery, cacheRefusals, defaultEffort, effortToSend, CLAUDE_EFFORTS } from './api.js';
|
|
19
19
|
import { hasRipgrep } from './utils/shell.js';
|
|
20
20
|
import { displayPath } from './utils/paths.js';
|
|
21
21
|
import { rewind } from './checkpoints.js';
|
|
@@ -23,8 +23,10 @@ import { canRestart, runSelfCheck } from './restart.js';
|
|
|
23
23
|
|
|
24
24
|
const EFFORTS = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
25
25
|
const isGpt = (modelId) => /^gpt-/i.test(modelId);
|
|
26
|
-
// The levels /effort offers for a model:
|
|
26
|
+
// The levels /effort offers for a model: Claude's own (as in Claude Code); for a GPT model the gateway's metadata,
|
|
27
|
+
// else the local Codex cache; EFFORTS for other models (an Anthropic-compatible API may take any of them).
|
|
27
28
|
const effortLevels = (modelId, modelInfo = null) => {
|
|
29
|
+
if (/^claude-/i.test(modelId)) return CLAUDE_EFFORTS;
|
|
28
30
|
const remote = gatewayEfforts(modelInfo);
|
|
29
31
|
if (remote.length) return remote;
|
|
30
32
|
return isGpt(modelId) && codexEfforts(modelId).length ? codexEfforts(modelId) : EFFORTS;
|
|
@@ -272,10 +274,8 @@ export function buildCommands(app) {
|
|
|
272
274
|
if (!choice) {
|
|
273
275
|
const configured = (cfg.env.CLAUDE_CODE_EFFORT_LEVEL || '').trim().toLowerCase() || 'auto';
|
|
274
276
|
const fallback = defaultEffort(id, cfg);
|
|
275
|
-
// A level this
|
|
276
|
-
const sent = cfg.effort
|
|
277
|
-
? modelInfo ? gatewayEffort(modelInfo, cfg.effort) : codexEffort(id, cfg.effort)
|
|
278
|
-
: cfg.effort;
|
|
277
|
+
// A level this model does not take goes out as the nearest one it does.
|
|
278
|
+
const sent = effortToSend(cfg, id, cfg.effort);
|
|
279
279
|
const current = !cfg.effort ? c.bold(`auto (${fallback ? `model default: ${fallback}` : 'not sent'})`)
|
|
280
280
|
: `${c.bold(cfg.effort)}${sent !== cfg.effort ? c.yellow(` (${id} does not take it: sent as ${sent})`) : ''}`;
|
|
281
281
|
choice = await app.showDialog(new SelectDialog({
|
package/src/gateway.js
CHANGED
|
@@ -69,11 +69,26 @@ export async function fetchGatewayAccount({ url, apiKey }, { fetchImpl = globalT
|
|
|
69
69
|
|
|
70
70
|
// The models the gateway serves (GET /v1/models), fetched once per gateway and key. The reasoning fields are
|
|
71
71
|
// optional so older gateways still work; when present they are kept with the model for /effort and requests.
|
|
72
|
+
// Every GPT request waits for it the first time, so it gives up after a few seconds, and after a failure it is not
|
|
73
|
+
// asked again for a minute: a gateway whose /v1/models fails must not slow down every request.
|
|
72
74
|
const modelLists = new Map();
|
|
73
|
-
|
|
75
|
+
const modelListFailures = new Map(); // key -> {at, error}
|
|
76
|
+
const MODELS_TIMEOUT_MS = 5000;
|
|
77
|
+
const MODELS_RETRY_MS = 60_000;
|
|
78
|
+
export async function fetchGatewayModels({ url, apiKey }, { fetchImpl = globalThis.fetch, signal, timeoutMs = MODELS_TIMEOUT_MS } = {}) {
|
|
74
79
|
const key = `${normalizeGatewayUrl(url)}\n${apiKey}`;
|
|
75
80
|
if (modelLists.has(key)) return modelLists.get(key);
|
|
76
|
-
const
|
|
81
|
+
const failed = modelListFailures.get(key);
|
|
82
|
+
if (failed && Date.now() - failed.at < MODELS_RETRY_MS) throw failed.error;
|
|
83
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
84
|
+
let body;
|
|
85
|
+
try {
|
|
86
|
+
body = await gatewayJson(`${normalizeGatewayUrl(url)}/v1/models`, { headers: { authorization: `Bearer ${apiKey}` }, signal: signal ? AbortSignal.any([signal, timeout]) : timeout }, fetchImpl);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
if (!signal?.aborted) modelListFailures.set(key, { at: Date.now(), error: e });
|
|
89
|
+
throw e;
|
|
90
|
+
}
|
|
91
|
+
modelListFailures.delete(key);
|
|
77
92
|
const list = (body.data || []).filter((m) => typeof m?.id === 'string').map((m) => ({
|
|
78
93
|
...m,
|
|
79
94
|
supported_reasoning_levels: (Array.isArray(m.supported_reasoning_levels) ? m.supported_reasoning_levels : Array.isArray(m.reasoning_levels) ? m.reasoning_levels : [])
|
package/src/tools/peers.js
CHANGED
|
@@ -22,7 +22,9 @@ export const ListPeersTool = {
|
|
|
22
22
|
|
|
23
23
|
export const MessagePeerTool = {
|
|
24
24
|
name: 'MessagePeer',
|
|
25
|
-
description: `Send a task or a question to another claude-pro session running on this machine (ListPeers lists them). The other session's agent works on it in its own conversation and directory, and the final text of its answer comes back to you later as a <peer-message kind="reply"> message: you do not wait for it here. Use it to delegate work that belongs to another session's project or context, or to ask it something only it knows. Write the message so that it stands on its own: the other agent has not seen your conversation
|
|
25
|
+
description: `Send a task or a question to another claude-pro session running on this machine (ListPeers lists them). The other session's agent works on it in its own conversation and directory, and the final text of its answer comes back to you later as a <peer-message kind="reply"> message: you do not wait for it here. Use it to delegate work that belongs to another session's project or context, or to ask it something only it knows. Write the message so that it stands on its own: the other agent has not seen your conversation.
|
|
26
|
+
|
|
27
|
+
Send it once and wait for the answer: do not send reminders or "please report" follow-ups while the other session works. Messages that arrive while it is busy are handled together after what it is doing, so they only add to its work; the answer comes by itself when it is done.`,
|
|
26
28
|
input_schema: {
|
|
27
29
|
type: 'object',
|
|
28
30
|
properties: {
|
package/src/ui/repl.js
CHANGED
|
@@ -16,9 +16,9 @@ import {
|
|
|
16
16
|
} from './render.js';
|
|
17
17
|
import { createRuntime } from '../runtime.js';
|
|
18
18
|
import { SessionStore, firstUserText } from '../session.js';
|
|
19
|
-
import { defaultEffort, effortSupported, quickQuery } from '../api.js';
|
|
19
|
+
import { defaultEffort, effortSupported, effortToSend, quickQuery } from '../api.js';
|
|
20
20
|
import { CONFIG_DIR, VERSION, APP_NAME, INSTALL_DIR, rememberChoice } from '../config.js';
|
|
21
|
-
import { readGatewayLogin
|
|
21
|
+
import { readGatewayLogin } from '../gateway.js';
|
|
22
22
|
import { TOPIC_TITLE_SYSTEM, topicTitlePrompt } from '../prompts.js';
|
|
23
23
|
import { buildCommands } from '../commands.js';
|
|
24
24
|
import { canRestart, writeRestartState, runSelfCheck, watchSource, restartNote, RESTART_EXIT_CODE, MAX_RESTARTS, RESTART_WINDOW_MS } from '../restart.js';
|
|
@@ -30,7 +30,6 @@ import { estimateTokens } from '../agent.js';
|
|
|
30
30
|
import { readClipboardImage, imageFromFile, watchClipboard } from '../utils/clipboard.js';
|
|
31
31
|
import { checkForUpdate } from '../updater.js';
|
|
32
32
|
import { PeerNode, peersEnabled, findPeer, peerMessagePrompt } from '../peers.js';
|
|
33
|
-
import { codexEffort } from '../codex/client.js';
|
|
34
33
|
|
|
35
34
|
// A model id as people say it: claude-sonnet-5-5 -> sonnet-5-5, claude-haiku-4-5-20251001 -> haiku-4-5.
|
|
36
35
|
export const modelTag = (model) => String(model?.id ?? '').replace(/^claude-/, '').replace(/-\d{8}$/, '');
|
|
@@ -39,13 +38,9 @@ export const modelTag = (model) => String(model?.id ?? '').replace(/^claude-/, '
|
|
|
39
38
|
// is left out when none is sent (auto) and for a model that does not take one.
|
|
40
39
|
export function modelInfo(config) {
|
|
41
40
|
const model = config.mainModel;
|
|
42
|
-
//
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
const chosen = config.effort && /^gpt-/i.test(model.id)
|
|
46
|
-
? gatewayModel ? gatewayEffort(gatewayModel, config.effort) : codexEffort(model.id, config.effort)
|
|
47
|
-
: config.effort;
|
|
48
|
-
const effort = chosen && effortSupported(model.id) ? chosen : null;
|
|
41
|
+
// What the requests carry: the nearest level the model takes (none goes out as low on gpt-6.1-sol, ultra as max on Claude).
|
|
42
|
+
const sent = effortToSend(config, model.id, config.effort);
|
|
43
|
+
const effort = sent && effortSupported(model.id) ? sent : null;
|
|
49
44
|
return { model: modelTag(model), effort };
|
|
50
45
|
}
|
|
51
46
|
|
|
@@ -387,7 +382,11 @@ export class App {
|
|
|
387
382
|
const text = error ? `The session failed before finishing: ${error.message || error}`
|
|
388
383
|
: result?.interrupted ? `The user of ${this.peers.name} interrupted the work before it was finished.${result.text ? `\n\nWhat it had written:\n${result.text}` : ''}`
|
|
389
384
|
: (result?.text || '(no answer text)');
|
|
390
|
-
for
|
|
385
|
+
// One answer per session, to its last message: several messages from it that waited for this turn were all
|
|
386
|
+
// handled in it, and sending the whole answer once for each would hand that session the same text again and again.
|
|
387
|
+
const lastOfEach = new Map();
|
|
388
|
+
for (const msg of tasks) lastOfEach.set(`${msg.from.id}|${msg.from.name}|${msg.from.cwd}`, msg);
|
|
389
|
+
for (const msg of lastOfEach.values()) {
|
|
391
390
|
// by id, or by name: a session that was restarted by older code comes back with a new id
|
|
392
391
|
const running = this.peers.peers();
|
|
393
392
|
const peer = findPeer(running, msg.from.id) || running.find((p) => p.name === msg.from.name && p.cwd === msg.from.cwd);
|