termdock 1.4.38 → 1.4.40

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.
Files changed (78) hide show
  1. package/dist/client/assets/{abnfDiagram-VRR7QNED-DsRHqRKj.js → abnfDiagram-VRR7QNED-1BbN_Bdc.js} +1 -1
  2. package/dist/client/assets/{arc-D5aRHB8f.js → arc-B4X31X3m.js} +1 -1
  3. package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-FbRXS-d6.js → architectureDiagram-ZJ3FMSHR-BlauOFgG.js} +1 -1
  4. package/dist/client/assets/{blockDiagram-677ZJIJ3-loWzVfeN.js → blockDiagram-677ZJIJ3-B0mtlYc1.js} +1 -1
  5. package/dist/client/assets/{c4Diagram-LMCZKHZV-z6T_C8bO.js → c4Diagram-LMCZKHZV-B52qGtrK.js} +1 -1
  6. package/dist/client/assets/channel-D2AA77kQ.js +1 -0
  7. package/dist/client/assets/{chunk-2Q5K7J3B-CqG-SvII.js → chunk-2Q5K7J3B-TkkyEbTr.js} +1 -1
  8. package/dist/client/assets/{chunk-32BRIVSS-0zL-WEd8.js → chunk-32BRIVSS-rmkLGfWN.js} +1 -1
  9. package/dist/client/assets/{chunk-5VM5RSS4-CPYKdlUm.js → chunk-5VM5RSS4-CifeRLjF.js} +1 -1
  10. package/dist/client/assets/{chunk-EX3LRPZG-DJ1zpvJe.js → chunk-EX3LRPZG-B_bint1e.js} +1 -1
  11. package/dist/client/assets/{chunk-JWPE2WC7-Ckoj6xXY.js → chunk-JWPE2WC7-DOZwisqR.js} +1 -1
  12. package/dist/client/assets/{chunk-MOJQB5TN-CA3Nm7t1.js → chunk-MOJQB5TN-BEWrO_py.js} +1 -1
  13. package/dist/client/assets/{chunk-RYQCIY6F-C-ifDv4n.js → chunk-RYQCIY6F-DM20-r1x.js} +1 -1
  14. package/dist/client/assets/{chunk-V7JOEXUC-B-FIe5fo.js → chunk-V7JOEXUC-06fOLFGY.js} +1 -1
  15. package/dist/client/assets/{chunk-VR4S4FIN-CmIFAiFd.js → chunk-VR4S4FIN-C9ivxt4F.js} +1 -1
  16. package/dist/client/assets/{chunk-XXDRQBXY-CdoSUjQU.js → chunk-XXDRQBXY-BU98Kf52.js} +1 -1
  17. package/dist/client/assets/classDiagram-OUVF2IWQ-DFpEzsId.js +1 -0
  18. package/dist/client/assets/classDiagram-v2-EOCWNBFH-DFpEzsId.js +1 -0
  19. package/dist/client/assets/{cose-bilkent-JH36ORCC-CHU0yaXN.js → cose-bilkent-JH36ORCC-036xKocd.js} +1 -1
  20. package/dist/client/assets/{cynefin-VYW2F7L2-D6-Ma9sC.js → cynefin-VYW2F7L2-Nqf06Kmu.js} +1 -1
  21. package/dist/client/assets/{cynefinDiagram-TSTJHNR4-90QatrQq.js → cynefinDiagram-TSTJHNR4-Qa8iUoZZ.js} +1 -1
  22. package/dist/client/assets/{dagre-VKFMJZFB-DdKyNy6i.js → dagre-VKFMJZFB-q5f0Dltl.js} +1 -1
  23. package/dist/client/assets/{diagram-FQU43EPY-9Hb1AzJf.js → diagram-FQU43EPY-CIVowYlH.js} +1 -1
  24. package/dist/client/assets/{diagram-G47NLZAW-CYvxq8hr.js → diagram-G47NLZAW-Dv1vCJ3c.js} +1 -1
  25. package/dist/client/assets/{diagram-NH7WQ7WH-BwOK3wm7.js → diagram-NH7WQ7WH-lLyO_C5q.js} +1 -1
  26. package/dist/client/assets/{diagram-OA4YK3LP-69v6bBie.js → diagram-OA4YK3LP-3cNxfhEy.js} +1 -1
  27. package/dist/client/assets/{diagram-WEI45ONY-CM-W_1QO.js → diagram-WEI45ONY-BLRqKjFK.js} +1 -1
  28. package/dist/client/assets/{ebnfDiagram-CCIWWBDH-DF49qxpN.js → ebnfDiagram-CCIWWBDH-CsSbfhPV.js} +1 -1
  29. package/dist/client/assets/{erDiagram-Q63AITRT-CoLDcoFF.js → erDiagram-Q63AITRT-Dx9mks1A.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-23GEKE2U-y8NvyxFE.js → flowDiagram-23GEKE2U-CqDiBbzG.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-NO4QXBWP-CJPYo7TO.js → ganttDiagram-NO4QXBWP-Ch-l1-IG.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-Dpd4PkU9.js → gitGraphDiagram-IHSO6WYX-CRhTdG6q.js} +1 -1
  33. package/dist/client/assets/index-B2_LqlRR.js +430 -0
  34. package/dist/client/assets/index-BQkNIHH4.css +1 -0
  35. package/dist/client/assets/{infoDiagram-FWYZ7A6U-B8EUPVad.js → infoDiagram-FWYZ7A6U-B3fKiraO.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-CvRk7Maz.js → ishikawaDiagram-FXEZZL3T-DBG_zMtG.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-5HDEW3XC-nBMG1OD3.js → journeyDiagram-5HDEW3XC-Bl1jwFh0.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-HUTT4EX6-CgVb7EQi.js → kanban-definition-HUTT4EX6-D0bYnuMX.js} +1 -1
  39. package/dist/client/assets/{linear-8cbNA5xR.js → linear-Bqm0-Qpp.js} +1 -1
  40. package/dist/client/assets/{mermaid.core-oQNFhFo1.js → mermaid.core-EBsM-F0c.js} +4 -4
  41. package/dist/client/assets/{mindmap-definition-LN4V7U3C-DNjz8EZH.js → mindmap-definition-LN4V7U3C-BQin-jQj.js} +1 -1
  42. package/dist/client/assets/{pegDiagram-2B236MQR-DYqUqAEf.js → pegDiagram-2B236MQR-HTTycFMY.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-ENE6RG2P-CP_uLGw0.js → pieDiagram-ENE6RG2P-V0Je9c4E.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-DFiVbYb8.js → quadrantDiagram-ABIIQ3AL-C3BCyDnb.js} +1 -1
  45. package/dist/client/assets/{railroadDiagram-RFXS5EU6-B-2UsGs8.js → railroadDiagram-RFXS5EU6-CFBxVWwn.js} +1 -1
  46. package/dist/client/assets/{requirementDiagram-TGXJPOKE-CljBWOcX.js → requirementDiagram-TGXJPOKE-OCuv5jiF.js} +1 -1
  47. package/dist/client/assets/{sankeyDiagram-HTMAVEWB-Dv1Zufob.js → sankeyDiagram-HTMAVEWB-CJDTF9-0.js} +1 -1
  48. package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-DXKZXUnf.js → sequenceDiagram-DBY2YBRQ-DNsYn886.js} +1 -1
  49. package/dist/client/assets/{sizeCapture-X5ZJPWSS-D2ZCuHz7.js → sizeCapture-X5ZJPWSS-UCObVrPF.js} +1 -1
  50. package/dist/client/assets/{stateDiagram-2N3HPSRC-CLxfz-m7.js → stateDiagram-2N3HPSRC-DRVv_Dm9.js} +1 -1
  51. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-BhnNNo6s.js +1 -0
  52. package/dist/client/assets/{swimlanes-5IMT3BWC-BYW0DkzS.js → swimlanes-5IMT3BWC-B3pf_PPr.js} +2 -2
  53. package/dist/client/assets/swimlanesDiagram-G3AALYLV-By5sWNT_.js +8 -0
  54. package/dist/client/assets/{timeline-definition-FHXFAJF6-CcTyB2mH.js → timeline-definition-FHXFAJF6-DtXbFtyH.js} +1 -1
  55. package/dist/client/assets/{vennDiagram-L72KCM5P-BkB73IYt.js → vennDiagram-L72KCM5P-D5Ln-8Wo.js} +1 -1
  56. package/dist/client/assets/{wardleyDiagram-EHGQE667-Bj3vIf4c.js → wardleyDiagram-EHGQE667-DBg6Xz6w.js} +1 -1
  57. package/dist/client/assets/{xychartDiagram-FW5EYKEG-D0koRTet.js → xychartDiagram-FW5EYKEG-_x4UP6WR.js} +1 -1
  58. package/dist/client/index.html +2 -2
  59. package/dist/client/sw.js +1 -1
  60. package/dist/server/cli.js +55 -0
  61. package/dist/server/quota/QuotaManager.js +142 -0
  62. package/dist/server/quota/providers/claude.js +125 -0
  63. package/dist/server/quota/providers/codex.js +175 -0
  64. package/dist/server/quota/providers/copilot.js +125 -0
  65. package/dist/server/quota/providers/gemini.js +52 -0
  66. package/dist/server/quota/providers/grok.js +76 -0
  67. package/dist/server/quota/providers/kimi.js +113 -0
  68. package/dist/server/quota/tokenDiscovery.js +239 -0
  69. package/dist/server/quota/types.js +8 -0
  70. package/dist/server/routes/terminal.js +76 -1
  71. package/package.json +1 -1
  72. package/dist/client/assets/channel-Cxs9-lsF.js +0 -1
  73. package/dist/client/assets/classDiagram-OUVF2IWQ-C9M6wlWI.js +0 -1
  74. package/dist/client/assets/classDiagram-v2-EOCWNBFH-C9M6wlWI.js +0 -1
  75. package/dist/client/assets/index-CN9znI-m.js +0 -430
  76. package/dist/client/assets/index-d23ibKJx.css +0 -1
  77. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-qmGgoGYE.js +0 -1
  78. package/dist/client/assets/swimlanesDiagram-G3AALYLV-DGIJpGeD.js +0 -8
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Token discovery — reads auth tokens from agent configuration files.
3
+ *
4
+ * Termdock's hook installers already know where each agent stores its config.
5
+ * This module extracts OAuth tokens / API keys so the QuotaManager can call
6
+ * provider usage APIs without asking the user to re-enter credentials.
7
+ */
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import { homedir } from 'os';
11
+ const HOME = homedir();
12
+ // ── Claude / Anthropic ──────────────────────────────────────────────
13
+ /** Read ~/.local/share/opencode/auth.json for Anthropic OAuth tokens. */
14
+ function discoverFromOpenCodeAuth() {
15
+ const results = [];
16
+ const authPath = path.join(HOME, '.local/share/opencode/auth.json');
17
+ try {
18
+ const raw = fs.readFileSync(authPath, 'utf-8');
19
+ const auth = JSON.parse(raw);
20
+ const oauths = Array.isArray(auth) ? auth : ('oauth' in auth ? auth.oauth : [auth]);
21
+ const entries = Array.isArray(oauths) ? oauths : [oauths];
22
+ for (const entry of entries) {
23
+ if (entry.type === 'anthropic' && entry.access) {
24
+ results.push({
25
+ token: entry.access,
26
+ source: 'opencode auth.json',
27
+ accountId: entry.accountId || entry.accountLabel || undefined,
28
+ });
29
+ }
30
+ }
31
+ }
32
+ catch {
33
+ // File missing or unreadable — no Anthropic tokens from this source.
34
+ }
35
+ return results;
36
+ }
37
+ /** Read ~/.claude/settings.json for stored credentials. */
38
+ function discoverFromClaudeSettings() {
39
+ const results = [];
40
+ const settingsPath = path.join(HOME, '.claude/settings.json');
41
+ try {
42
+ const raw = fs.readFileSync(settingsPath, 'utf-8');
43
+ const settings = JSON.parse(raw);
44
+ // Claude Code stores OAuth credentials in various shapes.
45
+ // The most reliable source is the oauth account info.
46
+ const oauth = settings?.oauthAccount;
47
+ if (oauth?.accessToken) {
48
+ results.push({
49
+ token: oauth.accessToken,
50
+ source: 'claude settings.json',
51
+ email: oauth.email || undefined,
52
+ accountId: oauth.accountUuid || undefined,
53
+ });
54
+ }
55
+ }
56
+ catch {
57
+ // Not found or unreadable.
58
+ }
59
+ return results;
60
+ }
61
+ export function discoverClaudeTokens() {
62
+ const fromOpenCode = discoverFromOpenCodeAuth().filter((t) => t.source === 'opencode auth.json');
63
+ // OpenCode auth.json already covers Anthropic — avoid duplicates.
64
+ if (fromOpenCode.length > 0) {
65
+ // Merge with claude settings for email enrichment
66
+ const fromClaude = discoverFromClaudeSettings();
67
+ for (const t of fromOpenCode) {
68
+ const match = fromClaude.find((c) => c.email && c.token !== t.token);
69
+ if (match && !t.email) {
70
+ t.email = match.email;
71
+ t.accountId = match.accountId;
72
+ }
73
+ }
74
+ return fromOpenCode;
75
+ }
76
+ return discoverFromClaudeSettings();
77
+ }
78
+ // ── Codex / OpenAI ───────────────────────────────────────────────────
79
+ function discoverCodexFromOpenCodeAuth() {
80
+ const results = [];
81
+ const authPath = path.join(HOME, '.local/share/opencode/auth.json');
82
+ try {
83
+ const raw = fs.readFileSync(authPath, 'utf-8');
84
+ const auth = JSON.parse(raw);
85
+ const oauths = Array.isArray(auth) ? auth : ('oauth' in auth ? auth.oauth : [auth]);
86
+ const entries = Array.isArray(oauths) ? oauths : [oauths];
87
+ for (const entry of entries) {
88
+ if ((entry.type === 'openai' || entry.type === 'chatgpt' || entry.type === 'codex') && entry.access) {
89
+ results.push({
90
+ token: entry.access,
91
+ source: 'opencode auth.json',
92
+ accountId: entry.accountId || undefined,
93
+ });
94
+ }
95
+ }
96
+ }
97
+ catch {
98
+ // Not found.
99
+ }
100
+ return results;
101
+ }
102
+ /** Read ~/.codex/auth.json for Codex CLI native OAuth tokens. */
103
+ function discoverCodexFromCodexAuth() {
104
+ const results = [];
105
+ const authPath = path.join(HOME, '.codex', 'auth.json');
106
+ try {
107
+ const raw = fs.readFileSync(authPath, 'utf-8');
108
+ const auth = JSON.parse(raw);
109
+ const tokens = auth?.tokens;
110
+ if (tokens?.access_token) {
111
+ results.push({
112
+ token: tokens.access_token,
113
+ source: 'codex auth.json',
114
+ accountId: tokens.account_id || undefined,
115
+ });
116
+ }
117
+ }
118
+ catch {
119
+ // Not found.
120
+ }
121
+ return results;
122
+ }
123
+ function discoverCodexFromEnv() {
124
+ const results = [];
125
+ const envVars = ['OPENAI_API_KEY', 'CODEX_API_KEY', 'CHATGPT_TOKEN'];
126
+ for (const v of envVars) {
127
+ const val = process.env[v];
128
+ if (val && val.trim()) {
129
+ results.push({ token: val.trim(), source: `env:${v}` });
130
+ }
131
+ }
132
+ return results;
133
+ }
134
+ export function discoverCodexTokens() {
135
+ // Priority: Codex CLI auth > OpenCode auth > env
136
+ const fromCodex = discoverCodexFromCodexAuth();
137
+ if (fromCodex.length > 0)
138
+ return fromCodex;
139
+ const fromOpenCode = discoverCodexFromOpenCodeAuth();
140
+ if (fromOpenCode.length > 0)
141
+ return fromOpenCode;
142
+ return discoverCodexFromEnv();
143
+ }
144
+ // ── Kimi ─────────────────────────────────────────────────────────────
145
+ export function discoverKimiTokens() {
146
+ const results = [];
147
+ // Check KIMI_API_KEY env
148
+ const envKey = process.env.KIMI_API_KEY;
149
+ if (envKey?.trim()) {
150
+ results.push({ token: envKey.trim(), source: 'env:KIMI_API_KEY' });
151
+ }
152
+ // Check ~/.kimi-code config
153
+ const configPath = path.join(HOME, '.kimi-code', 'config.json');
154
+ try {
155
+ const raw = fs.readFileSync(configPath, 'utf-8');
156
+ const cfg = JSON.parse(raw);
157
+ if (cfg.apiKey) {
158
+ results.push({ token: cfg.apiKey, source: 'kimi-code config.json' });
159
+ }
160
+ }
161
+ catch {
162
+ // Not found.
163
+ }
164
+ // Check OpenCode auth.json for kimi type
165
+ const authPath = path.join(HOME, '.local/share/opencode/auth.json');
166
+ try {
167
+ const raw = fs.readFileSync(authPath, 'utf-8');
168
+ const auth = JSON.parse(raw);
169
+ const entries = Array.isArray(auth) ? auth : [auth];
170
+ for (const entry of entries) {
171
+ if (entry.type === 'kimi' && entry.access) {
172
+ results.push({ token: entry.access, source: 'opencode auth.json' });
173
+ }
174
+ }
175
+ }
176
+ catch {
177
+ // Not found.
178
+ }
179
+ return results;
180
+ }
181
+ // ── GitHub Copilot ───────────────────────────────────────────────────
182
+ export function discoverCopilotTokens() {
183
+ const results = [];
184
+ // OpenCode auth.json
185
+ const authPath = path.join(HOME, '.local/share/opencode/auth.json');
186
+ try {
187
+ const raw = fs.readFileSync(authPath, 'utf-8');
188
+ const auth = JSON.parse(raw);
189
+ const entries = Array.isArray(auth) ? auth : [auth];
190
+ for (const entry of entries) {
191
+ if ((entry.type === 'github' || entry.type === 'copilot') && entry.access) {
192
+ results.push({
193
+ token: entry.access,
194
+ source: 'opencode auth.json',
195
+ accountId: entry.accountId || entry.accountLabel || undefined,
196
+ });
197
+ }
198
+ }
199
+ }
200
+ catch {
201
+ // Not found.
202
+ }
203
+ // Copilot CLI config
204
+ const cliConfigPath = path.join(HOME, '.copilot', 'config.json');
205
+ try {
206
+ const raw = fs.readFileSync(cliConfigPath, 'utf-8');
207
+ const cfg = JSON.parse(raw);
208
+ if (cfg.token) {
209
+ results.push({ token: cfg.token, source: 'copilot config.json' });
210
+ }
211
+ }
212
+ catch {
213
+ // Not found.
214
+ }
215
+ // GH_TOKEN env
216
+ const ghToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
217
+ if (ghToken?.trim()) {
218
+ results.push({ token: ghToken.trim(), source: 'env:GITHUB_TOKEN' });
219
+ }
220
+ return results;
221
+ }
222
+ // ── Gemini ────────────────────────────────────────────────────────────
223
+ export function discoverGeminiTokens() {
224
+ const results = [];
225
+ const envKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
226
+ if (envKey?.trim()) {
227
+ results.push({ token: envKey.trim(), source: `env:GEMINI_API_KEY` });
228
+ }
229
+ return results;
230
+ }
231
+ // ── Grok ──────────────────────────────────────────────────────────────
232
+ export function discoverGrokTokens() {
233
+ const results = [];
234
+ const envKey = process.env.GROK_API_KEY || process.env.XAI_API_KEY;
235
+ if (envKey?.trim()) {
236
+ results.push({ token: envKey.trim(), source: `env:GROK_API_KEY` });
237
+ }
238
+ return results;
239
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Subscription quota tracking types.
3
+ *
4
+ * Each provider's fetch() returns a QuotaResult with usage percentage,
5
+ * remaining/entitlement for quota-based plans, and optional per-window
6
+ * breakdowns (e.g. Claude's 5h/7d windows, Codex's primary/secondary).
7
+ */
8
+ export {};
@@ -24,6 +24,7 @@ import { listAllHookAgents, refreshStaleHooksAtLaunch, installHooksForSlug, unin
24
24
  import { loadPlugins, savePlugin, removePlugin, readPluginIcon, } from '../agent/plugins.js';
25
25
  import { registerPluginAgents } from '../agent/registry.js';
26
26
  import { notifyAgentTransition } from '../notifications/pushService.js';
27
+ import { getQuotaStatus, refreshQuota, startQuotaManager, } from '../quota/QuotaManager.js';
27
28
  const router = express.Router();
28
29
  const execFileAsync = promisify(execFile);
29
30
  const TERMDOCK_DIR = `${os.homedir()}/.termdock`;
@@ -2145,6 +2146,11 @@ function persistAgentResumeBinding(backendSessionId, session) {
2145
2146
  upsertGlobalSessionRecord({ ...record, agentResume: next });
2146
2147
  schedulePersistGlobalState();
2147
2148
  }
2149
+ /** Debounce window before treating an agent disappearance as a real exit.
2150
+ * Subcommands that briefly occupy the foreground group cause momentary poll
2151
+ * blips — waiting 2–3 poll cycles (1200 ms each) before clearing the session
2152
+ * avoids false "agent exited" notifications while keeping real exits responsive. */
2153
+ const AGENT_EXIT_DEBOUNCE_MS = 3000;
2148
2154
  /**
2149
2155
  * React to activeProgram changes: detect agent identity, end the rich session
2150
2156
  * when the agent leaves the foreground, and stamp the launch argv (for resume
@@ -2157,15 +2163,33 @@ function syncAgentIdentity(sessionId, session) {
2157
2163
  // The agent leaving the foreground ends its session: clear the rich state
2158
2164
  // so a stale "waiting" dot can't outlive the process. The poll can blip
2159
2165
  // momentarily (an agent-spawned subcommand takes the foreground group),
2160
- // but events re-establish state on the next signal.
2166
+ // so we debounce: only clear after AGENT_EXIT_DEBOUNCE_MS of continuous
2167
+ // absence. A real agent swap (agent A → agent B) clears immediately.
2161
2168
  if (session.agent && session.agentSession) {
2169
+ if (detected === null) {
2170
+ // Agent disappeared — could be a poll blip (subcommand) or real exit.
2171
+ const now = Date.now();
2172
+ if (session.agentLeftAt === null) {
2173
+ session.agentLeftAt = now;
2174
+ return;
2175
+ }
2176
+ if (now - session.agentLeftAt < AGENT_EXIT_DEBOUNCE_MS) {
2177
+ return;
2178
+ }
2179
+ }
2180
+ // Debounce expired (or agent swapped): clear the session for real.
2162
2181
  persistAgentResumeBinding(sessionId, session);
2163
2182
  session.lastAgentReviewed = session.agentSession.reviewed;
2164
2183
  session.agentSession = null;
2184
+ session.agentLeftAt = null;
2165
2185
  }
2166
2186
  session.agent = detected;
2167
2187
  broadcastAgentStatus(sessionId, session);
2168
2188
  }
2189
+ else if (detected && session.agentLeftAt !== null) {
2190
+ // Agent re-detected before debounce expired — false alarm, restore.
2191
+ session.agentLeftAt = null;
2192
+ }
2169
2193
  // Stamp the launch argv the identity poll captured — resume gets the flags.
2170
2194
  if (detected && session.activeProgram?.rawArgs) {
2171
2195
  const argv = splitCommandToArgv(session.activeProgram.rawArgs);
@@ -2196,6 +2220,9 @@ function syncAgentIdentity(sessionId, session) {
2196
2220
  function applyAgentSignals(sessionId, session, events, notification) {
2197
2221
  if (events.length === 0 && !notification)
2198
2222
  return;
2223
+ // Hook events prove the agent process is alive — cancel any pending-exit timer
2224
+ // that a poll blip may have started in syncAgentIdentity.
2225
+ session.agentLeftAt = null;
2199
2226
  let identityChanged = false;
2200
2227
  for (const event of events) {
2201
2228
  // An event naming an agent brands the pane even when the process poll
@@ -3507,6 +3534,7 @@ async function spawnTerminalSession(req, input) {
3507
3534
  agent: null,
3508
3535
  agentSession: null,
3509
3536
  lastAgentReviewed: null,
3537
+ agentLeftAt: null,
3510
3538
  gitStatus: null,
3511
3539
  gitStatusKey: null,
3512
3540
  gitAgentActivitySeen: 0,
@@ -3587,6 +3615,7 @@ async function adoptPtyHostSessions() {
3587
3615
  agent: null,
3588
3616
  agentSession: null,
3589
3617
  lastAgentReviewed: null,
3618
+ agentLeftAt: null,
3590
3619
  gitStatus: null,
3591
3620
  gitStatusKey: null,
3592
3621
  gitAgentActivitySeen: 0,
@@ -4272,6 +4301,52 @@ router.get('/agent-plugin-icon/:slug', (req, res) => {
4272
4301
  res.send(svg);
4273
4302
  });
4274
4303
  // ── end Agent plugins API ──
4304
+ // ── Subscription quota API ──
4305
+ function broadcastQuotaToAll() {
4306
+ const status = getQuotaStatus();
4307
+ const payload = {
4308
+ type: 'quota-status',
4309
+ providers: status.providers,
4310
+ updatedAt: status.updatedAt,
4311
+ };
4312
+ const data = JSON.stringify(payload);
4313
+ // Send to control WS clients
4314
+ for (const [clientId, ws] of controlClients) {
4315
+ if (ws.readyState !== ws.OPEN) {
4316
+ controlClients.delete(clientId);
4317
+ continue;
4318
+ }
4319
+ try {
4320
+ ws.send(data);
4321
+ }
4322
+ catch {
4323
+ controlClients.delete(clientId);
4324
+ }
4325
+ }
4326
+ // Send to per-session WS clients
4327
+ for (const [sessionId] of terminalSessions) {
4328
+ try {
4329
+ broadcastJsonWs(sessionId, payload);
4330
+ }
4331
+ catch { /* best-effort */ }
4332
+ }
4333
+ }
4334
+ // Start quota polling — broadcast fn hooks into both WS channels
4335
+ startQuotaManager({ broadcast: broadcastQuotaToAll });
4336
+ router.get('/quota', (_req, res) => {
4337
+ res.json(getQuotaStatus());
4338
+ });
4339
+ router.post('/quota/refresh', async (_req, res) => {
4340
+ try {
4341
+ const status = await refreshQuota();
4342
+ broadcastQuotaToAll();
4343
+ res.json(status);
4344
+ }
4345
+ catch (e) {
4346
+ res.status(500).json({ error: e instanceof Error ? e.message : 'Refresh failed' });
4347
+ }
4348
+ });
4349
+ // ── Subscription quota API end ──
4275
4350
  // ── Program detection config API ──
4276
4351
  router.get('/program-detection', async (_req, res) => {
4277
4352
  res.json(await loadProgramDetectionFromDisk());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termdock",
3
- "version": "1.4.38",
3
+ "version": "1.4.40",
4
4
  "description": "A complete web-based terminal application with modern UI",
5
5
  "type": "module",
6
6
  "main": ".desktop-dist/main.js",
@@ -1 +0,0 @@
1
- import{U as a,C as n}from"./mermaid.core-oQNFhFo1.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,a as s,c as t,C as e}from"./chunk-V7JOEXUC-B-FIe5fo.js";import{b as i}from"./mermaid.core-oQNFhFo1.js";import"./chunk-5VM5RSS4-CPYKdlUm.js";import"./chunk-XXDRQBXY-CdoSUjQU.js";import"./chunk-VR4S4FIN-CmIFAiFd.js";import"./chunk-32BRIVSS-0zL-WEd8.js";import"./purify.es-CC4Brkr_.js";import"./index-CN9znI-m.js";import"./react-XDCjal2T.js";import"./dnd-BCFAPC0L.js";import"./terminal-C7X98Amf.js";var g={parser:t,get db(){return new e},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{g as diagram};
@@ -1 +0,0 @@
1
- import{s as a,a as s,c as t,C as e}from"./chunk-V7JOEXUC-B-FIe5fo.js";import{b as i}from"./mermaid.core-oQNFhFo1.js";import"./chunk-5VM5RSS4-CPYKdlUm.js";import"./chunk-XXDRQBXY-CdoSUjQU.js";import"./chunk-VR4S4FIN-CmIFAiFd.js";import"./chunk-32BRIVSS-0zL-WEd8.js";import"./purify.es-CC4Brkr_.js";import"./index-CN9znI-m.js";import"./react-XDCjal2T.js";import"./dnd-BCFAPC0L.js";import"./terminal-C7X98Amf.js";var g={parser:t,get db(){return new e},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{g as diagram};