nterminal 1.3.15 → 1.3.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/MarkdownPreview-CJLdG6sf.js +1 -0
- package/dist/client/assets/{TranscriptMarkdownBody-CcknjOWW.js → TranscriptMarkdownBody-OpqkfDIn.js} +1 -1
- package/dist/client/assets/{index-B4ZOdvVw.js → index-7sIa7Kq3.js} +1 -1
- package/dist/client/assets/index-BYm1_iBq.js +59 -0
- package/dist/client/index.html +1 -1
- package/dist/server/accounts/accountService.d.ts +4 -0
- package/dist/server/accounts/accountService.js +123 -9
- package/dist/server/accounts/accountService.js.map +1 -1
- package/dist/server/accounts/codexDaemonAuth.d.ts +17 -0
- package/dist/server/accounts/codexDaemonAuth.js +177 -0
- package/dist/server/accounts/codexDaemonAuth.js.map +1 -0
- package/dist/server/accounts/loginSession.d.ts +1 -0
- package/dist/server/accounts/loginSession.js +16 -5
- package/dist/server/accounts/loginSession.js.map +1 -1
- package/dist/server/routes/agentManagementRoutes.js +7 -4
- package/dist/server/routes/agentManagementRoutes.js.map +1 -1
- package/dist/server/terminal/PtyAdapter.d.ts +7 -4
- package/dist/server/terminal/PtyAdapter.js.map +1 -1
- package/dist/server/terminal/TerminalManager.js +7 -0
- package/dist/server/terminal/TerminalManager.js.map +1 -1
- package/dist/server/terminal/TmuxPtyAdapter.d.ts +2 -5
- package/dist/server/terminal/TmuxPtyAdapter.js +20 -12
- package/dist/server/terminal/TmuxPtyAdapter.js.map +1 -1
- package/dist/shared/protocol.d.ts +7 -0
- package/dist/shared/protocol.js.map +1 -1
- package/dist/shared/types.d.ts +1 -0
- package/docs/terminal-history.md +36 -4
- package/package.json +1 -1
- package/scripts/nterminal-tmux.conf +19 -8
- package/dist/client/assets/MarkdownPreview-DiD5zTx6.js +0 -1
- package/dist/client/assets/index-BiIblEKQ.js +0 -59
package/dist/client/index.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<link rel="icon" type="image/png" sizes="64x64" href="/icons/favicon-64.png" />
|
|
13
13
|
<link rel="icon" type="image/png" sizes="96x96" href="/icons/favicon-96.png" />
|
|
14
14
|
<title>NTerminal</title>
|
|
15
|
-
<script type="module" crossorigin src="/assets/index-
|
|
15
|
+
<script type="module" crossorigin src="/assets/index-BYm1_iBq.js"></script>
|
|
16
16
|
<link rel="stylesheet" crossorigin href="/assets/index-D1X7cDQ9.css">
|
|
17
17
|
</head>
|
|
18
18
|
<body>
|
|
@@ -56,6 +56,7 @@ export declare class AccountService {
|
|
|
56
56
|
private readonly loginLauncher;
|
|
57
57
|
private readonly loginTimeoutMs;
|
|
58
58
|
private readonly loginExitWaitMs;
|
|
59
|
+
private readonly releaseCodexDaemon;
|
|
59
60
|
constructor(config: AppConfig, terminalManager: TerminalManager, options?: {
|
|
60
61
|
homeDir?: string;
|
|
61
62
|
now?: () => Date;
|
|
@@ -70,6 +71,7 @@ export declare class AccountService {
|
|
|
70
71
|
loginLauncher?: LoginLauncher;
|
|
71
72
|
loginTimeoutMs?: number;
|
|
72
73
|
loginExitWaitMs?: number;
|
|
74
|
+
releaseCodexDaemon?: (pid: number, mayStop: () => Promise<boolean>) => Promise<boolean>;
|
|
73
75
|
});
|
|
74
76
|
private mutate;
|
|
75
77
|
synchronize(): Promise<void>;
|
|
@@ -91,6 +93,8 @@ export declare class AccountService {
|
|
|
91
93
|
private removeUnlocked;
|
|
92
94
|
private beginLoginUnlocked;
|
|
93
95
|
private completeLoginUnlocked;
|
|
96
|
+
private markApplicationPending;
|
|
97
|
+
private applySelectedAccount;
|
|
94
98
|
private renameUnlocked;
|
|
95
99
|
private cancelLoginUnlocked;
|
|
96
100
|
private refreshUnlocked;
|
|
@@ -8,6 +8,7 @@ import { accountReloginReason } from '../../shared/accountRelogin.js';
|
|
|
8
8
|
import { ProviderRequestError, requestAccountUsage, requestTokenRefresh, unavailableUsage } from './providerRequests.js';
|
|
9
9
|
import { createPtyLoginLauncher, LOGIN_TIMEOUT_MS, LoginSessionError, ProviderLoginSession, validateLoginCode } from './loginSession.js';
|
|
10
10
|
import { readBootTimeSecondsAsync, readProcessStartTicksAsync } from '../terminal/procfs.js';
|
|
11
|
+
import { releaseIdleCodexDaemon } from './codexDaemonAuth.js';
|
|
11
12
|
const VAULT_DIRECTORY = 'credential-vault';
|
|
12
13
|
const VAULT_INDEX = 'index.json';
|
|
13
14
|
const VAULT_KEY = 'key';
|
|
@@ -76,6 +77,7 @@ export class AccountService {
|
|
|
76
77
|
loginLauncher;
|
|
77
78
|
loginTimeoutMs;
|
|
78
79
|
loginExitWaitMs;
|
|
80
|
+
releaseCodexDaemon;
|
|
79
81
|
constructor(config, terminalManager, options = {}) {
|
|
80
82
|
this.config = config;
|
|
81
83
|
this.terminalManager = terminalManager;
|
|
@@ -101,6 +103,8 @@ export class AccountService {
|
|
|
101
103
|
this.loginLauncher = options.loginLauncher ?? createPtyLoginLauncher();
|
|
102
104
|
this.loginTimeoutMs = options.loginTimeoutMs ?? LOGIN_TIMEOUT_MS;
|
|
103
105
|
this.loginExitWaitMs = options.loginExitWaitMs ?? LOGIN_COMPLETE_EXIT_WAIT_MS;
|
|
106
|
+
this.releaseCodexDaemon = options.releaseCodexDaemon ?? (options.processProbe || options.processLister || options.processSnapshot
|
|
107
|
+
? async () => false : (pid, mayStop) => releaseIdleCodexDaemon(this.homeDir, pid, mayStop));
|
|
104
108
|
}
|
|
105
109
|
// Reject overlapping mutations instead of accumulating requests behind OAuth I/O.
|
|
106
110
|
async mutate(operation) {
|
|
@@ -278,7 +282,7 @@ export class AccountService {
|
|
|
278
282
|
const pendingReady = new Map();
|
|
279
283
|
await Promise.all(['codex', 'claude'].map(async (provider) => {
|
|
280
284
|
const credentials = refreshed.pending[provider] ? await this.readCredentialFile(provider).catch(() => null) : null;
|
|
281
|
-
pendingReady.set(provider, { ready: Boolean(credentials), suggestedLabel: credentials ? suggestedLabel(refreshed, provider, credentials) : null });
|
|
285
|
+
pendingReady.set(provider, { ready: Boolean(credentials && hasOAuthCredential(provider, credentials)), suggestedLabel: credentials ? suggestedLabel(refreshed, provider, credentials) : null });
|
|
282
286
|
}));
|
|
283
287
|
this.expireLoginNotices();
|
|
284
288
|
return {
|
|
@@ -303,6 +307,9 @@ export class AccountService {
|
|
|
303
307
|
if (index.active[provider] === profile.id) {
|
|
304
308
|
if (clearExternalSwitch(index, provider))
|
|
305
309
|
await this.writeIndex(index);
|
|
310
|
+
const credentials = await this.readCredentialFile(provider);
|
|
311
|
+
if (credentials)
|
|
312
|
+
await this.applySelectedAccount(index, profile, credentials);
|
|
306
313
|
this.clearTerminationAcknowledgement(provider);
|
|
307
314
|
return this.status();
|
|
308
315
|
}
|
|
@@ -311,6 +318,7 @@ export class AccountService {
|
|
|
311
318
|
try {
|
|
312
319
|
await this.writeCredentialFile(provider, credentials);
|
|
313
320
|
index.active[provider] = profile.id;
|
|
321
|
+
this.markApplicationPending(index, profile);
|
|
314
322
|
clearExternalSwitch(index, provider);
|
|
315
323
|
await this.writeIndex(index);
|
|
316
324
|
}
|
|
@@ -324,6 +332,7 @@ export class AccountService {
|
|
|
324
332
|
// The selected profile's vault copy is exactly what was just published, so
|
|
325
333
|
// it already is the active backup. The previous account kept its archive.
|
|
326
334
|
this.clearTerminationAcknowledgement(provider);
|
|
335
|
+
await this.applySelectedAccount(index, profile, credentials);
|
|
327
336
|
return this.status();
|
|
328
337
|
}
|
|
329
338
|
async removeUnlocked(provider, profileId) {
|
|
@@ -401,27 +410,102 @@ export class AccountService {
|
|
|
401
410
|
const pending = index.pending[provider];
|
|
402
411
|
if (!pending)
|
|
403
412
|
throw new AccountServiceError('account_login_not_pending');
|
|
404
|
-
if (!await this.readCredentialFile(provider))
|
|
405
|
-
throw new AccountServiceError('account_login_not_complete');
|
|
406
413
|
// Let the CLI finish its own post-login writes before the file is captured.
|
|
407
414
|
const session = this.loginSessions.get(provider);
|
|
408
|
-
if (
|
|
409
|
-
|
|
415
|
+
if (!session)
|
|
416
|
+
throw new AccountServiceError('account_login_process_unavailable');
|
|
417
|
+
if (session.running)
|
|
418
|
+
await session.waitForExit(this.loginExitWaitMs);
|
|
419
|
+
if (!session.completed)
|
|
420
|
+
throw new AccountServiceError('account_login_not_complete');
|
|
410
421
|
const credentials = await this.readCredentialFile(provider);
|
|
411
|
-
if (!credentials)
|
|
422
|
+
if (!credentials || !hasOAuthCredential(provider, credentials))
|
|
412
423
|
throw new AccountServiceError('account_login_not_complete');
|
|
413
424
|
const profile = await this.storeProfile(index, provider, credentials, label);
|
|
414
425
|
// Back up the new active login before committing it as active.
|
|
415
426
|
await this.writeVaultCredentials(profile.id, credentials);
|
|
416
427
|
index.active[provider] = profile.id;
|
|
428
|
+
this.markApplicationPending(index, profile);
|
|
417
429
|
delete index.pending[provider];
|
|
418
430
|
clearExternalSwitch(index, provider);
|
|
419
431
|
await this.writeIndex(index);
|
|
420
432
|
if (this.loginSessions.get(provider) === session)
|
|
421
433
|
this.loginSessions.delete(provider);
|
|
422
434
|
this.clearTerminationAcknowledgement(provider);
|
|
435
|
+
await this.applySelectedAccount(index, profile, credentials);
|
|
423
436
|
return this.status();
|
|
424
437
|
}
|
|
438
|
+
markApplicationPending(index, profile) {
|
|
439
|
+
index.application ??= {};
|
|
440
|
+
index.application[profile.provider] = { profileId: profile.id, state: 'pending', reason: 'setup_failed', checkedAt: this.now().toISOString() };
|
|
441
|
+
}
|
|
442
|
+
// Credential storage is committed before runtime preparation. A failed check
|
|
443
|
+
// must never roll back a successful sign-in or delete its valid tokens.
|
|
444
|
+
async applySelectedAccount(index, profile, credentials) {
|
|
445
|
+
this.markApplicationPending(index, profile);
|
|
446
|
+
const application = index.application[profile.provider];
|
|
447
|
+
await this.writeIndex(index);
|
|
448
|
+
try {
|
|
449
|
+
if (!hasOAuthCredential(profile.provider, credentials))
|
|
450
|
+
throw new Error('missing credential');
|
|
451
|
+
const stillSelected = async () => {
|
|
452
|
+
const live = await this.readCredentialFile(profile.provider);
|
|
453
|
+
return live !== null && hashCredential(live) === hashCredential(credentials);
|
|
454
|
+
};
|
|
455
|
+
if (!await stillSelected())
|
|
456
|
+
throw new Error('credential changed');
|
|
457
|
+
if (profile.provider === 'claude') {
|
|
458
|
+
const settings = await this.readClaudeSettings();
|
|
459
|
+
if (settings.hasCompletedOnboarding !== true) {
|
|
460
|
+
if (this.runningPanes().some(pane => pane.provider === 'claude') || await this.processProbe('claude')) {
|
|
461
|
+
application.reason = 'sessions_running';
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (!await stillSelected())
|
|
465
|
+
throw new Error('credential changed');
|
|
466
|
+
const latest = await this.readClaudeSettings();
|
|
467
|
+
// Skip only the already-completed login wizard, never project trust,
|
|
468
|
+
// permissions, sandbox settings or user preferences.
|
|
469
|
+
await atomicWrite(path.join(this.homeDir, '.claude.json'), JSON.stringify({ ...latest, hasCompletedOnboarding: true }, null, 2), 0o600);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
const processes = await this.processLister('codex');
|
|
474
|
+
const daemons = processes.filter(isCodexDaemon);
|
|
475
|
+
if (processes.some(entry => !isCodexDaemon(entry)) || this.runningPanes().some(pane => pane.provider === 'codex')) {
|
|
476
|
+
application.reason = 'sessions_running';
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (daemons.length > 0) {
|
|
480
|
+
application.reason = 'daemon_unverified';
|
|
481
|
+
// Multiple homes/daemons are intentionally not guessed at.
|
|
482
|
+
if (daemons.length !== 1)
|
|
483
|
+
return;
|
|
484
|
+
const pid = daemons[0].pid;
|
|
485
|
+
const released = await this.releaseCodexDaemon(pid, async () => {
|
|
486
|
+
const latest = await this.processLister('codex');
|
|
487
|
+
return !this.disposed && await stillSelected()
|
|
488
|
+
&& !this.runningPanes().some(pane => pane.provider === 'codex')
|
|
489
|
+
&& latest.length === 1 && latest[0].pid === pid && isCodexDaemon(latest[0])
|
|
490
|
+
&& !(await this.listProcessSnapshot()).some(entry => entry.ppid === pid);
|
|
491
|
+
});
|
|
492
|
+
if (!released || (await this.processLister('codex')).length > 0)
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (!await stillSelected())
|
|
497
|
+
throw new Error('credential changed');
|
|
498
|
+
application.state = 'ready';
|
|
499
|
+
application.reason = null;
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
application.reason = 'setup_failed';
|
|
503
|
+
}
|
|
504
|
+
finally {
|
|
505
|
+
application.checkedAt = this.now().toISOString();
|
|
506
|
+
await this.writeIndex(index);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
425
509
|
async renameUnlocked(provider, profileId, label) {
|
|
426
510
|
const index = await this.readIndex();
|
|
427
511
|
const profile = index.profiles.find((candidate) => candidate.id === profileId && candidate.provider === provider);
|
|
@@ -457,6 +541,8 @@ export class AccountService {
|
|
|
457
541
|
await this.removeCredentialFile(provider);
|
|
458
542
|
}
|
|
459
543
|
delete index.pending[provider];
|
|
544
|
+
if (index.application)
|
|
545
|
+
delete index.application[provider];
|
|
460
546
|
await this.writeIndex(index);
|
|
461
547
|
this.clearTerminationAcknowledgement(provider);
|
|
462
548
|
return this.status();
|
|
@@ -527,15 +613,16 @@ export class AccountService {
|
|
|
527
613
|
loginState(provider, ready) {
|
|
528
614
|
const session = this.loginSessions.get(provider);
|
|
529
615
|
if (!session) {
|
|
530
|
-
return
|
|
616
|
+
return {
|
|
531
617
|
phase: 'failed',
|
|
532
618
|
message: 'The sign-in process is no longer running (NTerminal may have restarted). Cancel to restore the previous account, then try again.'
|
|
533
619
|
};
|
|
534
620
|
}
|
|
535
621
|
const state = session.state();
|
|
622
|
+
if (state.phase === 'failed')
|
|
623
|
+
return state;
|
|
536
624
|
if (ready) {
|
|
537
|
-
|
|
538
|
-
if (session.succeeded || !session.running)
|
|
625
|
+
if (session.completed)
|
|
539
626
|
return { phase: 'done' };
|
|
540
627
|
return { ...state, phase: 'completing' };
|
|
541
628
|
}
|
|
@@ -641,6 +728,7 @@ export class AccountService {
|
|
|
641
728
|
return {
|
|
642
729
|
provider,
|
|
643
730
|
activeProfileId: activeId,
|
|
731
|
+
...(index.application?.[provider]?.profileId === activeId && !pending ? { application: index.application[provider] } : {}),
|
|
644
732
|
profiles: index.profiles.filter((candidate) => candidate.provider === provider).map(toPublicProfile),
|
|
645
733
|
runningPanes: running.filter((pane) => pane.provider === provider),
|
|
646
734
|
runningProcesses,
|
|
@@ -711,7 +799,10 @@ export class AccountService {
|
|
|
711
799
|
if (index.pending[provider])
|
|
712
800
|
continue;
|
|
713
801
|
try {
|
|
802
|
+
const previousId = index.active[provider];
|
|
714
803
|
await this.captureActiveCredential(index, provider);
|
|
804
|
+
if (index.active[provider] !== previousId && index.application)
|
|
805
|
+
delete index.application[provider];
|
|
715
806
|
await this.backfillAccountEmails(index, provider);
|
|
716
807
|
this.diagnosticSignatures.delete(`account.refresh_scan:${provider}:`);
|
|
717
808
|
}
|
|
@@ -1056,6 +1147,7 @@ export class AccountService {
|
|
|
1056
1147
|
profiles,
|
|
1057
1148
|
active: isRecord(parsed.active) ? parsed.active : {},
|
|
1058
1149
|
pending: isRecord(parsed.pending) ? parsed.pending : {},
|
|
1150
|
+
application: parseApplications(parsed.application),
|
|
1059
1151
|
...(externalSwitch ? { externalSwitch } : {})
|
|
1060
1152
|
};
|
|
1061
1153
|
}
|
|
@@ -1426,6 +1518,28 @@ function clearExternalSwitch(index, provider) {
|
|
|
1426
1518
|
index.externalSwitch = rest;
|
|
1427
1519
|
return true;
|
|
1428
1520
|
}
|
|
1521
|
+
function hasOAuthCredential(provider, document) {
|
|
1522
|
+
const tokens = provider === 'codex' ? document.tokens : document.claudeAiOauth;
|
|
1523
|
+
if (!isRecord(tokens))
|
|
1524
|
+
return false;
|
|
1525
|
+
const access = provider === 'codex' ? tokens.access_token : tokens.accessToken;
|
|
1526
|
+
const refresh = provider === 'codex' ? tokens.refresh_token : tokens.refreshToken;
|
|
1527
|
+
return typeof access === 'string' && access.length > 0 && typeof refresh === 'string' && refresh.length > 0;
|
|
1528
|
+
}
|
|
1529
|
+
function parseApplications(value) {
|
|
1530
|
+
const result = {};
|
|
1531
|
+
if (!isRecord(value))
|
|
1532
|
+
return result;
|
|
1533
|
+
for (const provider of ['codex', 'claude']) {
|
|
1534
|
+
const entry = value[provider];
|
|
1535
|
+
if (isRecord(entry) && typeof entry.profileId === 'string' && typeof entry.checkedAt === 'string'
|
|
1536
|
+
&& (entry.state === 'ready' || entry.state === 'pending')
|
|
1537
|
+
&& (entry.reason === null || entry.reason === 'sessions_running' || entry.reason === 'daemon_unverified' || entry.reason === 'setup_failed')) {
|
|
1538
|
+
result[provider] = { profileId: entry.profileId, state: entry.state, reason: entry.reason, checkedAt: entry.checkedAt };
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
return result;
|
|
1542
|
+
}
|
|
1429
1543
|
function parseExternalSwitches(value) {
|
|
1430
1544
|
const result = {};
|
|
1431
1545
|
for (const provider of ['codex', 'claude']) {
|