@teamvibe/poller 0.8.4-rc.2 → 0.8.4
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/README.md +6 -6
- package/dist/claude-rate-limit.d.ts +1 -1
- package/dist/claude-rate-limit.js +3 -1
- package/dist/claude-spawner.js +2 -2
- package/dist/pool-telemetry.d.ts +4 -2
- package/dist/pool-telemetry.js +1 -0
- package/dist/redact-text.d.ts +2 -0
- package/dist/redact-text.js +15 -0
- package/dist/startup-auth.d.ts +1 -1
- package/dist/startup-auth.js +2 -14
- package/dist/subscription-selection.d.ts +2 -0
- package/dist/subscription-selection.js +28 -18
- package/dist/subscription-usage-store.js +11 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,23 +74,23 @@ All configuration is via environment variables (or `~/.teamvibe/.env`):
|
|
|
74
74
|
|----------|----------|---------|-------------|
|
|
75
75
|
| `TEAMVIBE_POLLER_TOKEN` | Yes | — | Your poller authentication token |
|
|
76
76
|
| `CLAUDE_CODE_OAUTH_TOKEN` | No | — | Claude Code OAuth token (from `claude setup-token`) |
|
|
77
|
-
| `EXPOSE_CLAUDE_TOKEN_TO_AGENT` | No | `
|
|
78
|
-
| `TEAMVIBE_CLAUDE_OAUTH_TOKEN` | Injected |
|
|
79
|
-
| `TEAMVIBE_POOL_ACCOUNT_ID` | Injected |
|
|
77
|
+
| `EXPOSE_CLAUDE_TOKEN_TO_AGENT` | No | `true` | Expose the session token to agent subprocesses by default; only `0`/`false`/`no`/`off` (case-insensitive) opt out. Unset or any other value is on |
|
|
78
|
+
| `TEAMVIBE_CLAUDE_OAUTH_TOKEN` | Injected | Session token | In the poller-injected env: the selected pool account's token or the poller's non-empty `CLAUDE_CODE_OAUTH_TOKEN`; unset on explicit opt-out or without a token. Inherited aliases are cleared. Claude settings can override this |
|
|
79
|
+
| `TEAMVIBE_POOL_ACCOUNT_ID` | Injected | Selected account ID | In the poller-injected env: the selected pool account ID (no secret); unset on explicit opt-out or without a pool account. Claude settings can override this |
|
|
80
80
|
| `TEAMVIBE_API_URL` | No | `https://poller.api.teamvibe.ai` | API endpoint |
|
|
81
81
|
| `MAX_CONCURRENT_SESSIONS` | No | `5` | Max parallel Claude sessions |
|
|
82
82
|
| `CLAUDE_CLI_PATH` | No | `claude` | Path to Claude Code CLI |
|
|
83
83
|
| `TEAMVIBE_DATA_DIR` | No | `~/.teamvibe` | Data directory for brains and state |
|
|
84
84
|
|
|
85
|
-
Set `EXPOSE_CLAUDE_TOKEN_TO_AGENT=
|
|
85
|
+
Agents can run nested Claude sessions on the current session's account by default, for both pool and non-pool sessions. Set `EXPOSE_CLAUDE_TOKEN_TO_AGENT=false` in the poller's environment to opt out (`0`, `no`, and `off` also disable it, case-insensitively); unset or any other value keeps it on. Claude CLI strips `CLAUDE_CODE_OAUTH_TOKEN` from Bash subprocess environments by name, so agents can restore it from the alias:
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
88
|
CLAUDE_CODE_OAUTH_TOKEN="$TEAMVIBE_CLAUDE_OAUTH_TOKEN" claude -p "…"
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
In the environment the poller injects, the alias and account ID follow the selected account on resume and failover;
|
|
91
|
+
In the environment the poller injects, the alias and account ID follow the selected account on resume and failover; with explicit opt-out, the poller leaves both variables unset. **This guarantee applies only to the injected process environment.** Claude CLI subsequently applies settings-file `env` entries: a brain's `CLAUDE_CONFIG_DIR/settings.json` can restore either variable when the flag is off or override it with a stale value when the flag is on. Project `.claude/settings.json` and `.claude/settings.local.json` can also override them. `settings.local.json` is a project source, not a user file next to `CLAUDE_CONFIG_DIR/settings.json`; recent CLI versions also read project-local settings from the main checkout for worktree sessions ([Claude settings sources](https://code.claude.com/docs/en/settings)). Before each spawn, the poller emits one warning listing the keys and files defining either variable in these settings sources, never their values. It never edits brain settings. Remove conflicting entries yourself to preserve the injected values in the agent's environment.
|
|
92
92
|
|
|
93
|
-
**Security trade-off:**
|
|
93
|
+
**Security trade-off:** by default, the agent and its subprocesses can read and use the token, bypassing Claude CLI's credential filtering. Opt out for agents you do not trust with that credential. With no settings override, nested sessions use the selected session account; this option adds no nested-session usage accounting. Poller-side Claude output logs, returned output/errors, typing status and fallback Slack replies redact the actual injected Claude token and alias values, plus assignments to either token variable name, whether exposure is enabled or explicitly disabled. This does not prevent agents from using credentials or sending them directly through their own tools.
|
|
94
94
|
|
|
95
95
|
## How It Works
|
|
96
96
|
|
|
@@ -9,7 +9,7 @@ export interface ClaudeRateLimitWindow {
|
|
|
9
9
|
export interface ClaudeRateLimitInfo extends ClaudeRateLimitWindow {
|
|
10
10
|
status?: string;
|
|
11
11
|
rateLimitType?: string;
|
|
12
|
-
unifiedWindows?: Partial<Record<
|
|
12
|
+
unifiedWindows?: Partial<Record<string, ClaudeRateLimitWindow>>;
|
|
13
13
|
overageStatus?: string;
|
|
14
14
|
overageDisabledReason?: string;
|
|
15
15
|
isUsingOverage?: boolean;
|
|
@@ -32,7 +32,9 @@ export function parseClaudeRateLimitInfo(value) {
|
|
|
32
32
|
const windows = record(obj['unifiedWindows']);
|
|
33
33
|
if (windows) {
|
|
34
34
|
const parsed = {};
|
|
35
|
-
for (const key of
|
|
35
|
+
for (const key of Object.keys(windows).slice(0, 128)) {
|
|
36
|
+
if (key.length > 128 || !/^(five_hour|seven_day)(?:_[a-z_]+)?$/.test(key))
|
|
37
|
+
continue;
|
|
36
38
|
const data = record(windows[key]);
|
|
37
39
|
if (data)
|
|
38
40
|
parsed[key] = windowInfo(data);
|
package/dist/claude-spawner.js
CHANGED
|
@@ -460,7 +460,7 @@ export function sweepProcessGroup(proc, graceMs, options = {}) {
|
|
|
460
460
|
* would look like if left uncapped.
|
|
461
461
|
*/
|
|
462
462
|
export function buildSpawnEnv(params) {
|
|
463
|
-
const exposeToken =
|
|
463
|
+
const exposeToken = !/^(0|false|no|off)$/i.test(params.processEnv['EXPOSE_CLAUDE_TOKEN_TO_AGENT'] ?? '');
|
|
464
464
|
const accountToken = params.subscriptionAccount?.credential.unwrap();
|
|
465
465
|
const sessionToken = accountToken ?? params.processEnv['CLAUDE_CODE_OAUTH_TOKEN'];
|
|
466
466
|
return {
|
|
@@ -513,7 +513,7 @@ export function buildSpawnEnv(params) {
|
|
|
513
513
|
// watched after the session (and worktree) is gone. See tv#265.
|
|
514
514
|
POLLER_BRAIN_PATH: params.brainPath,
|
|
515
515
|
// Claude strips its own token name from Bash subprocesses. This alias
|
|
516
|
-
//
|
|
516
|
+
// exposes the session token by default; the poller can explicitly opt out.
|
|
517
517
|
// Explicit undefined values also clear inherited/overlay aliases.
|
|
518
518
|
TEAMVIBE_CLAUDE_OAUTH_TOKEN: exposeToken ? sessionToken || undefined : undefined,
|
|
519
519
|
TEAMVIBE_POOL_ACCOUNT_ID: exposeToken ? params.subscriptionAccount?.accountId : undefined,
|
package/dist/pool-telemetry.d.ts
CHANGED
|
@@ -31,10 +31,11 @@ export declare function poolRunTelemetry(attempts: readonly PoolAttempt[], quota
|
|
|
31
31
|
score?: number | undefined;
|
|
32
32
|
reason?: "headroom_per_hour" | undefined;
|
|
33
33
|
rateWindow?: {
|
|
34
|
-
window: "five_hour" | "seven_day";
|
|
35
34
|
scope: "opus" | "haiku" | "global" | "sonnet" | "fable" | "uncertain";
|
|
36
35
|
percent: number | undefined;
|
|
37
36
|
resetAt: number | undefined;
|
|
37
|
+
kind?: "seven_day_overage_included" | "legacy-uncertain-seven-day" | undefined;
|
|
38
|
+
window: "five_hour" | "seven_day";
|
|
38
39
|
} | undefined;
|
|
39
40
|
accountId: string;
|
|
40
41
|
accountLabel: string;
|
|
@@ -48,10 +49,11 @@ export declare function poolRunTelemetry(attempts: readonly PoolAttempt[], quota
|
|
|
48
49
|
score?: number | undefined;
|
|
49
50
|
reason?: "headroom_per_hour" | undefined;
|
|
50
51
|
rateWindow?: {
|
|
51
|
-
window: "five_hour" | "seven_day";
|
|
52
52
|
scope: "opus" | "haiku" | "global" | "sonnet" | "fable" | "uncertain";
|
|
53
53
|
percent: number | undefined;
|
|
54
54
|
resetAt: number | undefined;
|
|
55
|
+
kind?: "seven_day_overage_included" | "legacy-uncertain-seven-day" | undefined;
|
|
56
|
+
window: "five_hour" | "seven_day";
|
|
55
57
|
} | undefined;
|
|
56
58
|
accountId?: string | undefined;
|
|
57
59
|
accountLabel?: string | undefined;
|
package/dist/pool-telemetry.js
CHANGED
|
@@ -86,6 +86,7 @@ export function poolRunTelemetry(attempts, quotaDeferEpisodes = 0) {
|
|
|
86
86
|
reason: a.reason,
|
|
87
87
|
rateWindow: {
|
|
88
88
|
window: a.rateWindow.window,
|
|
89
|
+
...(a.rateWindow.kind ? { kind: a.rateWindow.kind } : {}),
|
|
89
90
|
scope: a.rateWindow.scope,
|
|
90
91
|
percent: a.rateWindow.percent,
|
|
91
92
|
resetAt: a.rateWindow.resetAt,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Redact credentials in diagnostics without initializing poller authentication. */
|
|
2
|
+
export function redactStartupText(text, configuredTokens = []) {
|
|
3
|
+
let safe = text
|
|
4
|
+
.replace(/https?:\/\/[^\s/@]+@/gi, 'https://[REDACTED]@')
|
|
5
|
+
.replace(/\b(?:Authorization\s*:\s*)?Bearer\s+[^\s,;]+/gi, 'Bearer [REDACTED]')
|
|
6
|
+
.replace(/\bx-access-token\s*:\s*[^\s/@,;]+/gi, 'x-access-token:[REDACTED]')
|
|
7
|
+
// Cover both Claude's token name and the opt-in subprocess alias in
|
|
8
|
+
// shell assignments and JSON env diagnostics, even without a token list.
|
|
9
|
+
.replace(/(\b(?:CLAUDE_CODE_OAUTH_TOKEN|TEAMVIBE_CLAUDE_OAUTH_TOKEN)["']?\s*[:=]\s*)(?:"(?:\\.|[^"\\])*"|'[^']*'|[^\s,;]+)/gi, '$1[REDACTED]');
|
|
10
|
+
for (const token of configuredTokens) {
|
|
11
|
+
if (token)
|
|
12
|
+
safe = safe.split(token).join('[REDACTED]');
|
|
13
|
+
}
|
|
14
|
+
return safe;
|
|
15
|
+
}
|
package/dist/startup-auth.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export declare class UnknownStartupFailureError extends Error {
|
|
|
7
7
|
constructor(attempts: number);
|
|
8
8
|
}
|
|
9
9
|
/** The only formatter used for startup retry and terminal failure logs. */
|
|
10
|
-
export
|
|
10
|
+
export { redactStartupText } from './redact-text.js';
|
|
11
11
|
export declare function startupFailureDetail(error: unknown, operation: string, configuredTokens?: readonly string[]): string;
|
|
12
12
|
export declare function isPermanentStartupFailure(error: unknown): boolean;
|
|
13
13
|
/** Returns false when shutdown cancels startup. Permanent errors propagate to the caller. */
|
package/dist/startup-auth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { redactStartupText } from './redact-text.js';
|
|
1
2
|
import { AuthHttpError, StartupConfigurationError } from './auth-provider.js';
|
|
2
3
|
export class UnknownStartupFailureError extends Error {
|
|
3
4
|
attempts;
|
|
@@ -34,20 +35,7 @@ function isTransient(error) {
|
|
|
34
35
|
return false;
|
|
35
36
|
}
|
|
36
37
|
/** The only formatter used for startup retry and terminal failure logs. */
|
|
37
|
-
export
|
|
38
|
-
let safe = text
|
|
39
|
-
.replace(/https?:\/\/[^\s/@]+@/gi, 'https://[REDACTED]@')
|
|
40
|
-
.replace(/\b(?:Authorization\s*:\s*)?Bearer\s+[^\s,;]+/gi, 'Bearer [REDACTED]')
|
|
41
|
-
.replace(/\bx-access-token\s*:\s*[^\s/@,;]+/gi, 'x-access-token:[REDACTED]')
|
|
42
|
-
// Cover both Claude's token name and the opt-in subprocess alias in
|
|
43
|
-
// shell assignments and JSON env diagnostics, even without a token list.
|
|
44
|
-
.replace(/(\b(?:CLAUDE_CODE_OAUTH_TOKEN|TEAMVIBE_CLAUDE_OAUTH_TOKEN)["']?\s*[:=]\s*)(?:"(?:\\.|[^"\\])*"|'[^']*'|[^\s,;]+)/gi, '$1[REDACTED]');
|
|
45
|
-
for (const token of configuredTokens) {
|
|
46
|
-
if (token)
|
|
47
|
-
safe = safe.split(token).join('[REDACTED]');
|
|
48
|
-
}
|
|
49
|
-
return safe;
|
|
50
|
-
}
|
|
38
|
+
export { redactStartupText } from './redact-text.js';
|
|
51
39
|
export function startupFailureDetail(error, operation, configuredTokens = []) {
|
|
52
40
|
let detail;
|
|
53
41
|
if (error instanceof AuthHttpError)
|
|
@@ -35,6 +35,8 @@ export type PoolFetchResult<A extends AccountIdentity = AccountIdentity> = {
|
|
|
35
35
|
export interface UsageWindow {
|
|
36
36
|
window: 'five_hour' | 'seven_day';
|
|
37
37
|
scope: 'global' | 'sonnet' | 'opus' | 'haiku' | 'fable' | 'uncertain';
|
|
38
|
+
/** CLI kind, or the server compatibility provenance for a legacy Fable allowance. */
|
|
39
|
+
kind?: 'seven_day_overage_included' | 'legacy-uncertain-seven-day';
|
|
38
40
|
status?: 'allowed' | 'allowed_warning' | 'rejected';
|
|
39
41
|
/** Retained rejections keep their own clock when merged into newer measurements. */
|
|
40
42
|
observedAt?: number;
|
|
@@ -56,29 +56,24 @@ function date(value) {
|
|
|
56
56
|
const ms = Date.parse(value);
|
|
57
57
|
return Number.isFinite(ms) && ms > 0 ? ms : undefined;
|
|
58
58
|
}
|
|
59
|
+
/** Undocumented CLI stream kinds: only explicit model names and the measured Fable alias. */
|
|
60
|
+
function passiveWindowKind(kind) {
|
|
61
|
+
if (kind === 'seven_day_overage_included')
|
|
62
|
+
return { window: 'seven_day', scope: 'fable', kind };
|
|
63
|
+
const legacy = kind.match(/^(five_hour|seven_day)(?:_(sonnet|opus|haiku|fable))?$/);
|
|
64
|
+
return {
|
|
65
|
+
window: legacy?.[1] === 'five_hour' ? 'five_hour' : 'seven_day',
|
|
66
|
+
scope: legacy ? (legacy[2] ?? 'global') : 'uncertain',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
59
69
|
export function normalizePassiveUsage(value, observedAt) {
|
|
60
70
|
const info = record(value);
|
|
61
71
|
const windows = [];
|
|
62
|
-
const
|
|
63
|
-
for (const window of ['five_hour', 'seven_day']) {
|
|
64
|
-
const raw = unified[window];
|
|
65
|
-
if (raw == null)
|
|
66
|
-
continue;
|
|
67
|
-
const item = record(raw);
|
|
68
|
-
windows.push({ window, scope: 'global', percent: percent(item['utilization'], 1), resetAt: epoch(item['resetsAt']) });
|
|
69
|
-
}
|
|
70
|
-
const type = info['rateLimitType'];
|
|
71
|
-
if (typeof type === 'string') {
|
|
72
|
-
const legacy = type.match(/^(five_hour|seven_day)(?:_(sonnet|opus|haiku|fable))?$/);
|
|
73
|
-
const candidate = {
|
|
74
|
-
window: legacy?.[1] === 'five_hour' ? 'five_hour' : 'seven_day',
|
|
75
|
-
scope: legacy ? (legacy[2] ?? 'global') : 'uncertain',
|
|
76
|
-
percent: percent(info['utilization'], 1), resetAt: epoch(info['resetsAt']),
|
|
77
|
-
...(info['status'] === 'allowed' || info['status'] === 'allowed_warning' || info['status'] === 'rejected'
|
|
78
|
-
? { status: info['status'] } : {}),
|
|
79
|
-
};
|
|
72
|
+
const add = (candidate) => {
|
|
80
73
|
const existing = windows.find(w => w.window === candidate.window && w.scope === candidate.scope);
|
|
81
74
|
if (existing) {
|
|
75
|
+
if (candidate.kind !== undefined)
|
|
76
|
+
existing.kind = candidate.kind;
|
|
82
77
|
if (candidate.status !== undefined)
|
|
83
78
|
existing.status = candidate.status;
|
|
84
79
|
// Collapse the same quota while retaining conservative conflict evidence.
|
|
@@ -90,6 +85,21 @@ export function normalizePassiveUsage(value, observedAt) {
|
|
|
90
85
|
else {
|
|
91
86
|
windows.push(candidate);
|
|
92
87
|
}
|
|
88
|
+
};
|
|
89
|
+
for (const [kind, raw] of Object.entries(record(info['unifiedWindows'])).slice(0, 128)) {
|
|
90
|
+
if (raw == null)
|
|
91
|
+
continue;
|
|
92
|
+
const item = record(raw);
|
|
93
|
+
add({ ...passiveWindowKind(kind), percent: percent(item['utilization'], 1), resetAt: epoch(item['resetsAt']) });
|
|
94
|
+
}
|
|
95
|
+
const type = info['rateLimitType'];
|
|
96
|
+
if (typeof type === 'string') {
|
|
97
|
+
add({
|
|
98
|
+
...passiveWindowKind(type),
|
|
99
|
+
percent: percent(info['utilization'], 1), resetAt: epoch(info['resetsAt']),
|
|
100
|
+
...(info['status'] === 'allowed' || info['status'] === 'allowed_warning' || info['status'] === 'rejected'
|
|
101
|
+
? { status: info['status'] } : {}),
|
|
102
|
+
});
|
|
93
103
|
}
|
|
94
104
|
return { source: 'passive', observedAt, windows };
|
|
95
105
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { redactStartupText } from './redact-text.js';
|
|
1
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
3
|
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { join, resolve } from 'node:path';
|
|
@@ -34,12 +35,20 @@ function validTime(now) {
|
|
|
34
35
|
if (typeof now !== 'number' || !Number.isFinite(now) || now <= 0 || now > 253402300799000)
|
|
35
36
|
throw new SubscriptionUsageStoreError();
|
|
36
37
|
}
|
|
38
|
+
const loggedUnknownRateLimitKinds = new Set();
|
|
39
|
+
const knownRateLimitKind = /^(?:(five_hour|seven_day)(?:_(sonnet|opus|haiku|fable))?|seven_day_overage_included)$/;
|
|
37
40
|
export function safeRateLimitInfo(info) {
|
|
38
41
|
const parsed = parseClaudeRateLimitInfo(info) ?? {};
|
|
39
42
|
// Never persist arbitrary status strings or incidental stream fields.
|
|
40
43
|
if (parsed.status !== 'allowed' && parsed.status !== 'allowed_warning' && parsed.status !== 'rejected')
|
|
41
44
|
delete parsed.status;
|
|
42
|
-
|
|
45
|
+
for (const kind of [parsed.rateLimitType, ...Object.keys(parsed.unifiedWindows ?? {})]) {
|
|
46
|
+
if (!kind || kind === 'uncertain' || knownRateLimitKind.test(kind) || !/^[a-z_]{1,128}$/.test(kind) || loggedUnknownRateLimitKinds.has(kind))
|
|
47
|
+
continue;
|
|
48
|
+
loggedUnknownRateLimitKinds.add(kind);
|
|
49
|
+
console.warn(redactStartupText(`[Pool] unknown rate limit kind=${kind}`));
|
|
50
|
+
}
|
|
51
|
+
if (parsed.rateLimitType && !knownRateLimitKind.test(parsed.rateLimitType))
|
|
43
52
|
parsed.rateLimitType = 'uncertain';
|
|
44
53
|
delete parsed.overageStatus;
|
|
45
54
|
delete parsed.overageDisabledReason;
|
|
@@ -71,7 +80,7 @@ function safeUsage(value, source) {
|
|
|
71
80
|
if (observedAt !== undefined)
|
|
72
81
|
validTime(observedAt);
|
|
73
82
|
const source = w['source'] === 'passive' || w['source'] === 'probe' ? w['source'] : undefined;
|
|
74
|
-
return { window, scope, percent, resetAt, ...(status ? { status } : {}), ...(observedAt ? { observedAt } : {}), ...(source ? { source } : {}) };
|
|
83
|
+
return { window, scope, ...(w['kind'] === 'seven_day_overage_included' ? { kind: w['kind'] } : {}), percent, resetAt, ...(status ? { status } : {}), ...(observedAt ? { observedAt } : {}), ...(source ? { source } : {}) };
|
|
75
84
|
});
|
|
76
85
|
if (usage['windows'].length > 133)
|
|
77
86
|
windows.push({ window: 'seven_day', scope: 'uncertain' });
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teamvibe/poller",
|
|
3
|
-
"version": "0.8.4
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@teamvibe/poller",
|
|
9
|
-
"version": "0.8.4
|
|
9
|
+
"version": "0.8.4",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@aws-sdk/client-dynamodb": "3.1108.0",
|
|
12
12
|
"@aws-sdk/client-sqs": "3.1108.0",
|