claude-nonstop 0.3.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.
- package/.env.example +33 -0
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/assets/icon.jpeg +0 -0
- package/assets/screenshot.png +0 -0
- package/bin/claude-nonstop.js +1679 -0
- package/lib/config.js +163 -0
- package/lib/keychain.js +397 -0
- package/lib/platform.js +9 -0
- package/lib/reauth.js +147 -0
- package/lib/runner.js +566 -0
- package/lib/scorer.js +100 -0
- package/lib/service.js +196 -0
- package/lib/session.js +294 -0
- package/lib/tmux.js +95 -0
- package/lib/usage.js +146 -0
- package/package.json +56 -0
- package/remote/channel-manager.cjs +548 -0
- package/remote/hook-notify.cjs +504 -0
- package/remote/load-env.cjs +32 -0
- package/remote/paths.cjs +17 -0
- package/remote/start-webhook.cjs +97 -0
- package/remote/webhook.cjs +228 -0
- package/scripts/postinstall.js +40 -0
- package/slack-manifest.yaml +32 -0
package/lib/reauth.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared re-authentication helpers.
|
|
3
|
+
*
|
|
4
|
+
* Two-tier approach:
|
|
5
|
+
* 1. Silent refresh via OAuth token endpoint — uses the same endpoint and
|
|
6
|
+
* client ID as Claude Code CLI. Writes fresh tokens to the shared keychain.
|
|
7
|
+
* 2. Browser-based re-login via `claude auth login` — fallback when silent
|
|
8
|
+
* refresh fails (e.g., refresh token is also expired/revoked).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawn } from 'child_process';
|
|
12
|
+
import { readCredentials, isTokenExpired, refreshAccessToken } from './keychain.js';
|
|
13
|
+
import { DEFAULT_CLAUDE_DIR } from './config.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Attempt silent token refresh using the OAuth refresh endpoint.
|
|
17
|
+
*
|
|
18
|
+
* Uses the same endpoint and client ID as Claude Code CLI, writing
|
|
19
|
+
* fresh tokens back to the shared keychain. Both Claude Code and
|
|
20
|
+
* claude-nonstop see the updated credentials immediately.
|
|
21
|
+
*
|
|
22
|
+
* @param {{ name: string, configDir: string }} account
|
|
23
|
+
* @returns {Promise<boolean>} true if the token was refreshed successfully
|
|
24
|
+
*/
|
|
25
|
+
export async function silentRefresh(account) {
|
|
26
|
+
const result = await refreshAccessToken(account.configDir);
|
|
27
|
+
return !!result.token && !result.error;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Re-authenticate a single account via `claude auth login`.
|
|
32
|
+
* Opens the browser for OAuth — no interactive Claude session needed.
|
|
33
|
+
*
|
|
34
|
+
* @param {{ name: string, configDir: string }} account
|
|
35
|
+
* @returns {Promise<boolean>} true if credentials were refreshed successfully
|
|
36
|
+
*/
|
|
37
|
+
export async function reauthAccount(account) {
|
|
38
|
+
console.error(`\n[claude-nonstop] Re-authenticating "${account.name}"...`);
|
|
39
|
+
console.error(` Config: ${account.configDir}`);
|
|
40
|
+
console.error(' Opening browser for login...\n');
|
|
41
|
+
|
|
42
|
+
// Strip CLAUDECODE so this works when called from inside a Claude Code session.
|
|
43
|
+
// For the default account (~/.claude), don't set CLAUDE_CONFIG_DIR — Claude Code
|
|
44
|
+
// uses a hash-based keychain service name when the env var is explicitly set, even
|
|
45
|
+
// if it points to ~/.claude. This causes a mismatch where login writes to the
|
|
46
|
+
// hashed entry but readCredentials reads from the standard one.
|
|
47
|
+
const authEnv = { ...process.env };
|
|
48
|
+
delete authEnv.CLAUDECODE;
|
|
49
|
+
if (account.configDir === DEFAULT_CLAUDE_DIR) {
|
|
50
|
+
delete authEnv.CLAUDE_CONFIG_DIR;
|
|
51
|
+
} else {
|
|
52
|
+
authEnv.CLAUDE_CONFIG_DIR = account.configDir;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await new Promise((resolve) => {
|
|
56
|
+
const child = spawn('claude', ['auth', 'login'], {
|
|
57
|
+
env: authEnv,
|
|
58
|
+
stdio: 'inherit',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
child.on('close', () => resolve());
|
|
62
|
+
child.on('error', (err) => {
|
|
63
|
+
console.error(` Failed to launch Claude Code: ${err.message}`);
|
|
64
|
+
resolve();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const creds = readCredentials(account.configDir);
|
|
69
|
+
if (creds.token && !isTokenExpired(creds)) {
|
|
70
|
+
console.error(` "${account.name}" authenticated successfully.`);
|
|
71
|
+
if (creds.email) console.error(` Email: ${creds.email}`);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.error(` Warning: "${account.name}" still not authenticated.`);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Identify accounts needing re-auth and attempt to fix them interactively.
|
|
81
|
+
*
|
|
82
|
+
* Checks for:
|
|
83
|
+
* - Missing tokens (no credentials in keychain)
|
|
84
|
+
* - Expired tokens (expiresAt < now)
|
|
85
|
+
* - API-rejected tokens (usage API returned HTTP 401)
|
|
86
|
+
*
|
|
87
|
+
* Skips re-auth if stdin is not a TTY (non-interactive mode).
|
|
88
|
+
*
|
|
89
|
+
* @param {Array<{name: string, configDir: string, token?: string, usage?: object}>} accounts
|
|
90
|
+
* Accounts enriched with token and/or usage data
|
|
91
|
+
* @returns {Promise<string[]>} Names of accounts that were successfully re-authenticated
|
|
92
|
+
*/
|
|
93
|
+
export async function reauthExpiredAccounts(accounts) {
|
|
94
|
+
const needsReauth = accounts.filter(a => {
|
|
95
|
+
// No token at all
|
|
96
|
+
if (!a.token) return true;
|
|
97
|
+
// Token expired per keychain expiresAt
|
|
98
|
+
const creds = readCredentials(a.configDir);
|
|
99
|
+
if (isTokenExpired(creds)) return true;
|
|
100
|
+
// Usage API returned an auth error (401 expired, 403 revoked)
|
|
101
|
+
if (a.usage?.error === 'HTTP 401' || a.usage?.error === 'HTTP 403') return true;
|
|
102
|
+
return false;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (needsReauth.length === 0) return [];
|
|
106
|
+
|
|
107
|
+
const refreshed = [];
|
|
108
|
+
|
|
109
|
+
// First pass: try silent refresh (no browser, no TTY needed) for accounts
|
|
110
|
+
// that have tokens but are expired or rejected.
|
|
111
|
+
const silentCandidates = needsReauth.filter(a => a.token);
|
|
112
|
+
const stillNeedsReauth = [...needsReauth.filter(a => !a.token)];
|
|
113
|
+
|
|
114
|
+
if (silentCandidates.length > 0) {
|
|
115
|
+
console.error(`[claude-nonstop] Refreshing ${silentCandidates.length} expired token(s)...`);
|
|
116
|
+
for (const account of silentCandidates) {
|
|
117
|
+
if (await silentRefresh(account)) {
|
|
118
|
+
refreshed.push(account.name);
|
|
119
|
+
} else {
|
|
120
|
+
stillNeedsReauth.push(account);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (stillNeedsReauth.length === 0) return refreshed;
|
|
126
|
+
|
|
127
|
+
// Second pass: browser-based re-auth (requires TTY)
|
|
128
|
+
if (!process.stdin.isTTY) {
|
|
129
|
+
console.error('[claude-nonstop] Non-interactive mode — cannot open browser for re-auth. Run "claude-nonstop reauth" manually.');
|
|
130
|
+
return refreshed;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.error(`\n[claude-nonstop] ${stillNeedsReauth.length} account(s) need browser re-authentication:`);
|
|
134
|
+
for (const a of stillNeedsReauth) {
|
|
135
|
+
const reason = !a.token ? 'no credentials'
|
|
136
|
+
: a.usage?.error === 'HTTP 401' ? 'token rejected (401)'
|
|
137
|
+
: 'token expired';
|
|
138
|
+
console.error(` ${a.name}: ${reason}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const account of stillNeedsReauth) {
|
|
142
|
+
const success = await reauthAccount(account);
|
|
143
|
+
if (success) refreshed.push(account.name);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return refreshed;
|
|
147
|
+
}
|