tabby-agentdeck 1.1.2 → 1.2.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/CHANGELOG.md +150 -0
- package/README.ko.md +42 -0
- package/README.md +34 -545
- package/dist/agents.d.ts +15 -0
- package/dist/codexHooks.d.ts +29 -0
- package/dist/codexMeta.d.ts +87 -0
- package/dist/codexWheel.d.ts +6 -0
- package/dist/config.d.ts +120 -2
- package/dist/cursorVisibility.d.ts +41 -0
- package/dist/deck.service.d.ts +185 -19
- package/dist/devReload.service.d.ts +48 -0
- package/dist/devReload.stub.d.ts +19 -0
- package/dist/dock.d.ts +15 -0
- package/dist/gitDiff.d.ts +11 -0
- package/dist/i18n/af.d.ts +83 -0
- package/dist/i18n/bg.d.ts +83 -0
- package/dist/i18n/cs.d.ts +83 -0
- package/dist/i18n/da.d.ts +83 -0
- package/dist/i18n/de.d.ts +83 -0
- package/dist/i18n/en.d.ts +83 -0
- package/dist/i18n/es.d.ts +83 -0
- package/dist/i18n/fr.d.ts +83 -0
- package/dist/i18n/hr.d.ts +83 -0
- package/dist/i18n/id.d.ts +83 -0
- package/dist/i18n/it.d.ts +83 -0
- package/dist/i18n/ja.d.ts +83 -0
- package/dist/i18n/ko.d.ts +83 -0
- package/dist/i18n/pl.d.ts +83 -0
- package/dist/i18n/pt.d.ts +83 -0
- package/dist/i18n/ru.d.ts +83 -0
- package/dist/i18n/sr.d.ts +83 -0
- package/dist/i18n/sv.d.ts +83 -0
- package/dist/i18n/tr.d.ts +83 -0
- package/dist/i18n/uk.d.ts +83 -0
- package/dist/i18n/zh-CN.d.ts +83 -0
- package/dist/i18n/zh-TW.d.ts +83 -0
- package/dist/i18n.d.ts +3618 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -19591
- package/dist/keybind.d.ts +121 -0
- package/dist/meta.d.ts +83 -0
- package/dist/nav.d.ts +29 -0
- package/dist/notify.service.d.ts +196 -0
- package/dist/project-root.d.ts +1 -1
- package/dist/reload.service.d.ts +57 -0
- package/dist/screenAgent.d.ts +24 -0
- package/dist/sessionLedger.d.ts +15 -7
- package/dist/settings.component.d.ts +136 -8
- package/dist/status.service.d.ts +30 -2
- package/dist/statusLine.d.ts +27 -0
- package/dist/subagents.d.ts +31 -0
- package/dist/synchronizedOutput.d.ts +10 -0
- package/dist/terminalInput.d.ts +6 -0
- package/dist/update.d.ts +19 -5
- package/dist/update.service.d.ts +29 -20
- package/dist/viewPanel.d.ts +62 -2
- package/dist/viewer.d.ts +4 -0
- package/hooks/agentdeck-codex-notify.ps1 +28 -0
- package/hooks/agentdeck-label.js +1 -1
- package/hooks/agentdeck-notify.ps1 +112 -12
- package/hooks/agentdeck-statusline.mjs +351 -0
- package/package.json +22 -5
- package/updater/agentdeck-update.ps1 +50 -27
- package/dist/index.js.map +0 -1
|
@@ -34,12 +34,26 @@
|
|
|
34
34
|
# "StopFailure": -Status error 턴이 실패로 끝났다 (rate_limit 이면 limited)
|
|
35
35
|
#>
|
|
36
36
|
param(
|
|
37
|
+
# 어느 CLI 가 보냈나. **이 값이 에이전트 판정의 1순위 근거다** — 플러그인이 프로세스 이름·
|
|
38
|
+
# 명령줄·탭 제목·화면 문구로 추측하던 것(deck.service `detectAgentApp`)을 이 한 줄이 대신한다.
|
|
39
|
+
# 자기가 누구인지는 훅을 부르는 쪽이 아는 사실이지 추측할 일이 아니다 (cwd·tabId 와 같은 이치).
|
|
40
|
+
[ValidateSet('claude', 'codex', 'gemini')]
|
|
41
|
+
[string]$Agent = 'claude',
|
|
42
|
+
|
|
37
43
|
[ValidateSet('running', 'waiting', 'limited', 'done', 'error', 'idle')]
|
|
38
44
|
[string]$Status,
|
|
39
45
|
|
|
40
46
|
# 사람이 셸에서 직접 붙이는 작업 이름. 이때만 라벨을 보낸다
|
|
41
47
|
[string]$Label,
|
|
42
48
|
|
|
49
|
+
# 서브에이전트 생명주기. SubagentStart -> start / SubagentStop -> stop.
|
|
50
|
+
# `-Agent` 는 이미 어느 CLI 가 보냈나(claude/codex)에 쓰이므로 이름을 따로 둔다.
|
|
51
|
+
# **상태(-Status)와 섞지 않는다** — 이 이벤트는 세션 상태를 바꾸지 않고 개수만 움직인다.
|
|
52
|
+
# 훅 JSON 의 `agent_id` 가 함께 오므로 플러그인이 id 로 정확히 켜고 끈다(실측 payload:
|
|
53
|
+
# SubagentStart = agent_id·agent_type·session_id, SubagentStop = 같은 agent_id).
|
|
54
|
+
[ValidateSet('start', 'stop')]
|
|
55
|
+
[string]$Subagent,
|
|
56
|
+
|
|
43
57
|
# stdin 훅 JSON 없이 부를 때 대상 세션 (없으면 가장 최근에 갱신된 세션)
|
|
44
58
|
[string]$SessionId,
|
|
45
59
|
|
|
@@ -48,17 +62,30 @@ param(
|
|
|
48
62
|
# 없앤 순간 "알 수 없는 인자" 로 훅이 통째로 실패한다
|
|
49
63
|
[switch]$PromptAsLabel,
|
|
50
64
|
|
|
51
|
-
[int]$MaxLabel = 40
|
|
65
|
+
[int]$MaxLabel = 40,
|
|
66
|
+
|
|
67
|
+
# 같은 PowerShell 안에서 부르는 래퍼(agentdeck-codex-notify.ps1)가 훅 JSON 을 넘기는 자리.
|
|
68
|
+
# 파이프(`$json | & notify.ps1`)로 넘기지 않는 이유는 아래 stdin 주석 — 스크립트가 파이프 입력을
|
|
69
|
+
# 받는 순간 외부 호출(Claude Code)의 stdin 이 CP949 로 깨진다
|
|
70
|
+
[string]$HookJson
|
|
52
71
|
)
|
|
53
72
|
|
|
54
73
|
$ErrorActionPreference = 'SilentlyContinue'
|
|
55
74
|
|
|
56
75
|
try {
|
|
57
76
|
# PS 5.1 은 stdin 을 시스템 ANSI(CP949) 로 읽어 UTF-8 JSON 의 한글을 깨뜨린다.
|
|
58
|
-
# 표준입력 스트림을 UTF-8 로 직접 열어서
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
77
|
+
# 표준입력 스트림을 UTF-8 로 직접 열어서 읽는다.
|
|
78
|
+
#
|
|
79
|
+
# **이 파일 어디에도 파이프 입력 자동변수를 쓰지 말 것.** 스크립트가 그걸 참조하기만 해도
|
|
80
|
+
# PowerShell 호스트가 실행 전에 stdin 을 콘솔 인코딩(CP949)으로 먼저 다 읽어 가서, 아래 원본 스트림은
|
|
81
|
+
# 비고 파이프 입력 쪽은 이미 깨져 있다. 실측(2026-09-14): 참조하는 스크립트는 15/15 원본 스트림 0자,
|
|
82
|
+
# 참조하지 않는 스크립트는 15/15 2606자 온전 — 한도 문구의 `·` 가 `쨌` 로 깨지던 HK2·HK6 의 원인이었다
|
|
83
|
+
$raw = $HookJson
|
|
84
|
+
if (-not $raw) {
|
|
85
|
+
$reader = New-Object System.IO.StreamReader([Console]::OpenStandardInput(), (New-Object System.Text.UTF8Encoding($false)))
|
|
86
|
+
$raw = $reader.ReadToEnd()
|
|
87
|
+
$reader.Close()
|
|
88
|
+
}
|
|
62
89
|
$hook = if ($raw) { $raw | ConvertFrom-Json } else { $null }
|
|
63
90
|
} catch {
|
|
64
91
|
$hook = $null
|
|
@@ -108,11 +135,26 @@ if ($hook -and [string]$hook.hook_event_name -eq 'StopFailure') {
|
|
|
108
135
|
$failReason = if ($Status -eq 'limited' -and $hook.last_assistant_message) { [string]$hook.last_assistant_message } else { $code }
|
|
109
136
|
}
|
|
110
137
|
|
|
138
|
+
# --- 이번 도구가 고친 파일 ---
|
|
139
|
+
# `변경` 탭이 "이 세션이 만진 것만" 을 그리는 근거다. 저장소 전체 diff 는 P4 워크스페이스 위에
|
|
140
|
+
# git 을 얹은 곳에서 수백 개씩 나와 쓸모가 없다.
|
|
141
|
+
# Edit/Write/NotebookEdit 은 전부 `tool_input.file_path` 를 쓴다. 다른 도구에는 없으므로 그냥 비고,
|
|
142
|
+
# 훅이 아예 없는 에이전트는 이 키가 영영 안 와서 플러그인이 전체 목록으로 떨어진다(그게 폴백이다).
|
|
143
|
+
$touchedFile = ''
|
|
144
|
+
if ($hook -and $hook.tool_input -and $hook.tool_input.file_path) {
|
|
145
|
+
$touchedFile = [string]$hook.tool_input.file_path
|
|
146
|
+
}
|
|
147
|
+
|
|
111
148
|
# --- 진행중 중복 보고 생략 ---
|
|
112
149
|
# PostToolUse 는 도구가 돌 때마다 발화한다. 이미 running 인데 running 을 또 쓰면 파일·TCP 만 흔들고
|
|
113
150
|
# 사이드바는 변하지 않는다(경과시간이 매 도구마다 0 으로 되돌아가기까지 한다). 라벨 지정도 없으면 여기서 끝낸다 —
|
|
114
|
-
# 아래 계보 조회(~450ms)까지 건너뛰므로 도구마다 붙는 비용이 사실상
|
|
115
|
-
|
|
151
|
+
# 아래 계보 조회(~450ms)까지 건너뛰므로 도구마다 붙는 비용이 사실상 없다.
|
|
152
|
+
# **고친 파일이 있으면 건너뛰지 않는다** — 그 보고가 곧 `변경` 탭의 목록이라 하나라도 빠지면
|
|
153
|
+
# 그 파일이 화면에서 사라진다. 대신 그 경우에도 계보 조회는 하지 않는다(아래 tabId 분기):
|
|
154
|
+
# 세션은 이미 탭에 묶여 있어 sessionId 만으로 찾아간다(notify.service `resolveTab` 의 `alive`).
|
|
155
|
+
# **서브에이전트 이벤트도 건너뛰지 않는다** — 하나 빠지면 개수가 영구히 어긋난다(start 를 놓치면
|
|
156
|
+
# 적게, stop 을 놓치면 많게 굳는다). 이쪽도 계보 조회는 하지 않는다(아래 tabId 분기).
|
|
157
|
+
if ($Status -eq 'running' -and $prevStatus -eq 'running' -and -not $Label -and -not $touchedFile -and -not $Subagent) { exit 0 }
|
|
116
158
|
|
|
117
159
|
# --- 어느 탭인가: tabId (1순위) / 프로세스 계보 pids (폴백) ---
|
|
118
160
|
# 사이드바가 이 보고를 어느 탭에 붙일지 정하는 근거. 예전 규칙 "처음 보고할 때의 활성 탭" 은
|
|
@@ -139,9 +181,12 @@ $tabId = ([string]$env:AGENTDECK_TAB).Trim()
|
|
|
139
181
|
$pids = @()
|
|
140
182
|
$claudePid = 0
|
|
141
183
|
$claudeName = ''
|
|
184
|
+
# 이미 running 인 세션의 "파일만 알리는" 보고와 서브에이전트 이벤트에서는 계보를 재지 않는다 —
|
|
185
|
+
# 450ms 짜리 Win32_Process 전체 조회를 Edit·에이전트마다 낼 수는 없다. 그 세션은 앞선 보고로
|
|
186
|
+
# 이미 탭에 묶여 있어 sessionId 만으로 찾아간다 (notify.service `resolveTab` 의 `alive` 경로).
|
|
142
187
|
if ($tabId) {
|
|
143
188
|
# 탭이 확정됐다 — 계보는 계산하지 않는다
|
|
144
|
-
} elseif ($hook) {
|
|
189
|
+
} elseif ($hook -and -not $Subagent -and -not ($Status -eq 'running' -and $prevStatus -eq 'running')) {
|
|
145
190
|
$reuse = $false
|
|
146
191
|
if ($prev -and $prev.pids -and $prev.claudePid) {
|
|
147
192
|
$aliveProc = Get-Process -Id ([int]$prev.claudePid) -ErrorAction SilentlyContinue
|
|
@@ -186,7 +231,7 @@ if ($tabId) {
|
|
|
186
231
|
# 그래서 Stop(done) 뒤에 유휴 알림이 오면 완료된 탭이 '승인대기' 로 뒤집힌다 (2026-08-28 실측:
|
|
187
232
|
# status 폴더의 두 세션이 작업을 끝냈는데도 waiting 으로 박혀 있었다).
|
|
188
233
|
# 승인 요청이 아닌 알림이면 상태를 건드리지 않는다.
|
|
189
|
-
if ($Status -eq 'waiting') {
|
|
234
|
+
if ($Status -eq 'waiting' -and $Agent -eq 'claude') {
|
|
190
235
|
$msg = if ($hook -and $hook.message) { [string]$hook.message } else { '' }
|
|
191
236
|
$idleNotice = $msg -match '(?i)waiting for your input|idle'
|
|
192
237
|
$alreadyFinished = $prevStatus -eq 'done' -or $prevStatus -eq 'idle' -or $prevStatus -eq 'limited'
|
|
@@ -230,12 +275,64 @@ $effectiveStatus = if ($Status) { $Status } elseif ($prevStatus) { $prevStatus }
|
|
|
230
275
|
|
|
231
276
|
$payload = [ordered]@{
|
|
232
277
|
sessionId = $targetId
|
|
233
|
-
status = $effectiveStatus
|
|
234
278
|
ts = [int64](([datetime]::UtcNow - [datetime]'1970-01-01').TotalMilliseconds)
|
|
279
|
+
# 누가 보냈나. 상태·서브에이전트와 달리 **매번 같은 값**이지만 그래도 매번 싣는다 —
|
|
280
|
+
# 빼먹은 보고가 하나라도 있으면 플러그인이 그 탭을 다시 추측 경로로 떨어뜨린다.
|
|
281
|
+
# (서브에이전트 이벤트처럼 status 를 안 싣는 보고에도 들어가야 하므로 아래 분기 밖에 둔다)
|
|
282
|
+
agent = $Agent
|
|
235
283
|
}
|
|
284
|
+
# **서브에이전트 이벤트는 상태를 싣지 않는다.** 이 훅에는 `-Status` 가 없어서 직전 상태를
|
|
285
|
+
# 되쓰게 되는데, 그러면 이미 `done` 인 세션에 백그라운드 에이전트의 stop 이 도착할 때
|
|
286
|
+
# 세션이 `running` 으로 되살아난다 (백그라운드 에이전트는 턴이 끝난 뒤에도 돈다).
|
|
287
|
+
if (-not $Subagent) { $payload['status'] = $effectiveStatus }
|
|
236
288
|
if ($newLabel) { $payload['label'] = $newLabel }
|
|
237
289
|
if ($reason) { $payload['reason'] = $reason }
|
|
238
290
|
if ($tabId) { $payload['tabId'] = $tabId }
|
|
291
|
+
# --- 작업 폴더 ---
|
|
292
|
+
# 훅 JSON 의 `cwd` 는 에이전트가 실제로 돌고 있는 폴더다. 이걸 보내는 이유는 Tabby 순정의 cwd 추정이
|
|
293
|
+
# 윈도우에서 못 믿을 값을 내기 때문이다 — `guessWindowsCWD` 가 PTY 출력에서 `X:\...` 처럼 보이는
|
|
294
|
+
# 첫 토큰을 그대로 cwd 로 삼는다(tabby-local/dist/index.js:1316, 정규식 /([a-zA-Z]:[^\:\[\]\?\"\<\>\|]+)/mi).
|
|
295
|
+
# 에이전트가 화면에 찍은 **파일 경로**도 걸려서, 미리보기 `변경` 탭이 `git -C <파일.py>` 를 돌리고
|
|
296
|
+
# `fatal: cannot change to ...: Invalid argument` 로 끝났다 (2026-09-11 실측).
|
|
297
|
+
if ($hook -and $hook.cwd) { $payload['cwd'] = [string]$hook.cwd }
|
|
298
|
+
# 이번 도구가 고친 파일 — 플러그인이 탭별로 모아 `변경` 탭의 '세션' 목록을 만든다
|
|
299
|
+
if ($touchedFile) { $payload['file'] = $touchedFile }
|
|
300
|
+
|
|
301
|
+
# --- 지금 도는 서브에이전트 (스냅샷) ---
|
|
302
|
+
# **증분(이번에 하나 떴다/졌다)이 아니라 목록 전체를 보낸다.** 이유는 상태 파일이 "마지막 보고
|
|
303
|
+
# 한 건" 만 담기 때문이다 — TCP 가 막혀 폴링(400ms)으로만 전달되는 환경에서 start 와 stop 이
|
|
304
|
+
# 한 주기 안에 겹치면 앞의 것이 덮여 **영영 유실**되고, 누적으로 세는 개수는 그대로 어긋난다
|
|
305
|
+
# (상태값은 최신 하나만 의미가 있어서 이 구조로 충분했지만, 개수는 아니다).
|
|
306
|
+
# 목록을 통째로 실으면 덮여도 마지막 파일이 곧 정답이라 유실이 개수를 틀리게 만들지 못한다.
|
|
307
|
+
#
|
|
308
|
+
# 직전 목록은 이 파일에서 읽는다(= 훅 호출 사이의 저장소). 그래서 상태 보고(running/done)에서도
|
|
309
|
+
# 그대로 다시 실어 줘야 한다 — 안 실으면 다음 에이전트 이벤트가 빈 목록에서 시작한다.
|
|
310
|
+
$agents = @()
|
|
311
|
+
if ($prev -and $prev.agents) {
|
|
312
|
+
foreach ($a in @($prev.agents)) {
|
|
313
|
+
if ($a -and $a.id) {
|
|
314
|
+
$agents += , ([ordered]@{ id = [string]$a.id; type = [string]$a.type; at = [int64]$a.at })
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if ($Subagent -and $hook -and $hook.agent_id) {
|
|
319
|
+
$aid = [string]$hook.agent_id
|
|
320
|
+
# 같은 id 는 먼저 빼고 — start 가 두 번 와도 하나, stop 은 빼기만 하면 된다
|
|
321
|
+
$agents = @($agents | Where-Object { $_.id -ne $aid })
|
|
322
|
+
if ($Subagent -eq 'start') {
|
|
323
|
+
$agents += , ([ordered]@{
|
|
324
|
+
id = $aid
|
|
325
|
+
type = [string]$hook.agent_type
|
|
326
|
+
at = [int64](([datetime]::UtcNow - [datetime]'1970-01-01').TotalMilliseconds)
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
# 빈 목록도 보낸다 — "마지막 하나가 끝났다" 를 말할 방법이 이것뿐이다.
|
|
331
|
+
# 단 에이전트를 한 번도 본 적 없는 세션에서는 키 자체를 빼서, 훅이 개수를 안다고 주장하지 않는다
|
|
332
|
+
# (플러그인은 이 키를 본 탭에서만 대화기록 폴백을 끈다)
|
|
333
|
+
if ($Subagent -or $agents.Count -gt 0) {
|
|
334
|
+
$payload['agents'] = @($agents)
|
|
335
|
+
}
|
|
239
336
|
if ($pids.Count -gt 0) {
|
|
240
337
|
$payload['pids'] = [int[]]$pids
|
|
241
338
|
$payload['claudePid'] = $claudePid
|
|
@@ -246,8 +343,11 @@ if ($pids.Count -gt 0) {
|
|
|
246
343
|
#
|
|
247
344
|
# TCP 로 밀어 넣더라도 이 파일 쓰기는 건너뛰지 않는다 — 파일이 곧 상태 저장소라서
|
|
248
345
|
# 다음 훅 호출이 여기서 직전 상태를 읽는다(위 $prevStatus). 그게 '승인대기 오탐 차단' 의 근거다.
|
|
249
|
-
|
|
250
|
-
|
|
346
|
+
# `-Depth` 를 명시한다 — 기본 2 는 `agents` 안의 객체를 `System.Collections.Specialized...` 문자열로
|
|
347
|
+
# 뭉개 버린다(중첩 한 겹이 더 생겼다). 원소가 하나일 때 배열이 스칼라로 접히는 문제는 받는 쪽에서
|
|
348
|
+
# 배열/단일을 모두 받아 푼다 — PowerShell 의 JSON 변환은 그 접힘을 끌 수 없다
|
|
349
|
+
$json = $payload | ConvertTo-Json -Compress -Depth 5
|
|
350
|
+
$tmp = "$file.$PID.tmp"
|
|
251
351
|
$enc = New-Object System.Text.UTF8Encoding($false)
|
|
252
352
|
[System.IO.File]::WriteAllText($tmp, $json, $enc)
|
|
253
353
|
[System.IO.File]::Copy($tmp, $file, $true)
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code statusLine 을 한 겹 감싸서, 그 stdin JSON 을 AgentDeck 사이드바로 넘긴다.
|
|
3
|
+
*
|
|
4
|
+
* 왜 감싸나 — 모델·effort·컨텍스트%·5h/7d 한도% 는 **statusLine stdin 에만** 내려온다.
|
|
5
|
+
* 훅(JSON)에는 없고 대화기록(jsonl)에는 모델까지만 있다. 사이드바 하단의 "지금 이 탭" 줄이
|
|
6
|
+
* 원하는 값이 전부 여기 있으므로, 이미 걸려 있는 statusLine 명령 앞에 이 스크립트를 끼워
|
|
7
|
+
* 같은 stdin 을 그대로 흘려보내면서 한 벌 베껴 둔다.
|
|
8
|
+
*
|
|
9
|
+
* **이 스크립트의 첫 번째 계약은 "원래 statusLine 을 절대 죽이지 않는다" 이다.**
|
|
10
|
+
* statusLine 은 사용자가 매 렌더마다 보는 것이라, 우리가 실패해도 안쪽 명령의 출력은
|
|
11
|
+
* 그대로 나가야 한다. 그래서 베껴 쓰는 일은 전부 try/catch 로 감싸고, 종료코드도
|
|
12
|
+
* 안쪽 명령의 것을 그대로 쓴다. 안쪽이 없거나 못 띄우면 조용히 아무것도 출력하지 않는다
|
|
13
|
+
* (여기서 에러 문구를 뱉으면 그게 사용자의 statusLine 자리에 박힌다).
|
|
14
|
+
*
|
|
15
|
+
* 안쪽 명령은 설치할 때 `statusline-inner.json` 으로 옮겨 둔 **원래 설정 그대로**다
|
|
16
|
+
* (src/statusLine.ts). 체인 순서는 상관없다 — 이 스크립트는 stdin 을 바꾸지 않고
|
|
17
|
+
* stdout 을 건드리지 않으므로, 누가 우리를 또 감싸도 각 겹이 같은 JSON 을 본다.
|
|
18
|
+
*/
|
|
19
|
+
import * as fs from 'node:fs'
|
|
20
|
+
import * as os from 'node:os'
|
|
21
|
+
import * as path from 'node:path'
|
|
22
|
+
import { spawn } from 'node:child_process'
|
|
23
|
+
import { fileURLToPath } from 'node:url'
|
|
24
|
+
import * as https from 'node:https'
|
|
25
|
+
|
|
26
|
+
const ROOT = path.join(
|
|
27
|
+
process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
|
|
28
|
+
'tabby-agentdeck',
|
|
29
|
+
)
|
|
30
|
+
/** 사이드바가 읽는 곳. 상태 보고(`status/`)와 폴더를 나눈다 — 그쪽 폴링은 자기 형식만 알아본다 */
|
|
31
|
+
const META_DIR = path.join(ROOT, 'meta')
|
|
32
|
+
const INNER_FILE = path.join(ROOT, 'statusline-inner.json')
|
|
33
|
+
/** 모델별 주간 한도(Fable 등) 캐시 — stdin 에 없는 값이라 따로 물어본 것을 여기 둔다 */
|
|
34
|
+
const SCOPED_FILE = path.join(ROOT, 'scoped-limit.json')
|
|
35
|
+
/** 캐시를 낡았다고 보는 기준. statusLine 은 초 단위로 불리므로 매번 물어보면 안 된다 */
|
|
36
|
+
const SCOPED_TTL_MS = 60_000
|
|
37
|
+
/**
|
|
38
|
+
* **못 받았을 때는 더 오래 쉰다.**
|
|
39
|
+
*
|
|
40
|
+
* 실패는 대개 곧 풀리지 않는다 — 토큰이 없거나(API 키·게이트웨이), 그 요금제에 모델별 한도가
|
|
41
|
+
* 없거나, 레지스트리가 우리를 막은 경우다. 1분마다 두드리면 마지막 것은 **더 오래 막힌다**:
|
|
42
|
+
* 2026-09-15 실측으로 이 API 가 실제로 429 를 돌려줬다(앞서 캐시 도장이 없어 렌더마다
|
|
43
|
+
* 요청이 나가던 판의 결과다).
|
|
44
|
+
*/
|
|
45
|
+
const SCOPED_FAIL_TTL_MS = 10 * 60_000
|
|
46
|
+
|
|
47
|
+
function readStdin () {
|
|
48
|
+
try {
|
|
49
|
+
// fd 0 을 통째로 읽는다. Claude Code 는 JSON 한 덩이를 주고 stdin 을 닫는다
|
|
50
|
+
return fs.readFileSync(0)
|
|
51
|
+
} catch {
|
|
52
|
+
return Buffer.alloc(0)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 계정 — statusLine stdin 에는 없다. 설정 파일에서 읽는다.
|
|
58
|
+
*
|
|
59
|
+
* `CLAUDE_CONFIG_DIR` 이 걸려 있으면 그 아래를 먼저 본다. 그 환경변수는 이 프로세스가
|
|
60
|
+
* claude 에서 그대로 물려받은 것이라, **탭마다 계정을 갈라 쓰는 경우에도 이 값이 맞다.**
|
|
61
|
+
* (`$CLAUDE_CONFIG_DIR/.claude.json` 위치는 근거 미확인이라 홈 폴더로 폴백한다.)
|
|
62
|
+
*/
|
|
63
|
+
function readAccount () {
|
|
64
|
+
const dirs = []
|
|
65
|
+
if (process.env.CLAUDE_CONFIG_DIR) {
|
|
66
|
+
dirs.push(process.env.CLAUDE_CONFIG_DIR)
|
|
67
|
+
}
|
|
68
|
+
dirs.push(os.homedir())
|
|
69
|
+
for (const dir of dirs) {
|
|
70
|
+
try {
|
|
71
|
+
const file = path.join(dir, '.claude.json')
|
|
72
|
+
const acc = JSON.parse(fs.readFileSync(file, 'utf8'))?.oauthAccount
|
|
73
|
+
if (acc?.emailAddress) {
|
|
74
|
+
return {
|
|
75
|
+
account: String(acc.emailAddress),
|
|
76
|
+
org: acc.organizationName ? String(acc.organizationName) : '',
|
|
77
|
+
configDir: dir,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// 없거나 아직 못 읽는다 — 다음 후보로
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { account: '', org: '', configDir: '' }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function pct (v) {
|
|
88
|
+
const n = Number(v)
|
|
89
|
+
return Number.isFinite(n) ? Math.round(n) : null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------- 모델별 주간 한도 (Fable 등) ----------
|
|
93
|
+
//
|
|
94
|
+
// statusLine stdin 의 `rate_limits` 는 `five_hour` / `seven_day` 만 조립한다. 모델별 주간 한도
|
|
95
|
+
// (내부명 `seven_day_overage_included`, 화면 이름 `Fable`)는 **거기 없다** — 진본은
|
|
96
|
+
// `GET /api/oauth/usage` 의 `limits[]` 중 `kind=weekly_scoped` 다.
|
|
97
|
+
//
|
|
98
|
+
// 렌더를 막으면 안 되므로 **읽기는 캐시에서만** 한다. 캐시가 낡았으면 자기 자신을
|
|
99
|
+
// `--refresh-scoped` 로 떼어 내 띄우고(detached), 이번 렌더는 낡은 값 그대로 쓴다.
|
|
100
|
+
// 다음 렌더에 새 값이 들어온다 — statusLine 은 몇 초에 한 번씩 다시 불린다.
|
|
101
|
+
|
|
102
|
+
/** OAuth 토큰 — 계정 파일과 같은 후보 폴더를 본다 (탭마다 계정을 갈라 써도 맞는다) */
|
|
103
|
+
function readOauthToken () {
|
|
104
|
+
const dirs = []
|
|
105
|
+
if (process.env.CLAUDE_CONFIG_DIR) {
|
|
106
|
+
dirs.push(process.env.CLAUDE_CONFIG_DIR)
|
|
107
|
+
}
|
|
108
|
+
dirs.push(path.join(os.homedir(), '.claude'), os.homedir())
|
|
109
|
+
for (const dir of dirs) {
|
|
110
|
+
try {
|
|
111
|
+
const raw = fs.readFileSync(path.join(dir, '.credentials.json'), 'utf8')
|
|
112
|
+
const token = JSON.parse(raw)?.claudeAiOauth?.accessToken
|
|
113
|
+
if (token) {
|
|
114
|
+
return String(token)
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
// 없거나 못 읽는다 — 다음 후보로
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return ''
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function readScopedCache () {
|
|
124
|
+
try {
|
|
125
|
+
return JSON.parse(fs.readFileSync(SCOPED_FILE, 'utf8'))
|
|
126
|
+
} catch {
|
|
127
|
+
return null
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 캐시가 낡았으면 갱신을 떼어 낸다 — 부모(이 렌더)는 기다리지 않는다 */
|
|
132
|
+
function armScopedRefresh (cache) {
|
|
133
|
+
// 직전이 실패였으면(`ok: false`) 더 길게 쉰다 — SCOPED_FAIL_TTL_MS 주석
|
|
134
|
+
const ttl = cache && cache.ok === false ? SCOPED_FAIL_TTL_MS : SCOPED_TTL_MS
|
|
135
|
+
if (cache && Date.now() - Number(cache.ts || 0) < ttl) {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), '--refresh-scoped'], {
|
|
140
|
+
detached: true,
|
|
141
|
+
stdio: 'ignore',
|
|
142
|
+
env: { ...process.env, AGENTDECK_SL_DEPTH: '0' },
|
|
143
|
+
})
|
|
144
|
+
child.unref()
|
|
145
|
+
} catch {
|
|
146
|
+
// 못 띄우면 다음 렌더가 다시 시도한다
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 실제로 물어보고 캐시에 남긴다 (`--refresh-scoped` 로 따로 뜬 프로세스에서만 돈다).
|
|
152
|
+
*
|
|
153
|
+
* `fetch` 가 아니라 `node:https` 를 쓴다 — undici 는 응답을 다 읽은 뒤에도 소켓을 쥐고 있어서,
|
|
154
|
+
* 다 쓰고 `process.exit` 하면 Windows 에서 libuv 가 죽는다 (실측: `Assertion failed:
|
|
155
|
+
* !(handle->flags & UV_HANDLE_CLOSING)`, rc 127). 여기서는 응답이 끝나면 이벤트 루프가
|
|
156
|
+
* 저절로 비어 프로세스가 정상 종료된다.
|
|
157
|
+
*/
|
|
158
|
+
function requestUsage (token) {
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
const req = https.request({
|
|
161
|
+
host: 'api.anthropic.com',
|
|
162
|
+
path: '/api/oauth/usage',
|
|
163
|
+
method: 'GET',
|
|
164
|
+
headers: {
|
|
165
|
+
'authorization': `Bearer ${token}`,
|
|
166
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
167
|
+
'accept': 'application/json',
|
|
168
|
+
},
|
|
169
|
+
timeout: 10_000,
|
|
170
|
+
}, res => {
|
|
171
|
+
const chunks = []
|
|
172
|
+
res.on('data', c => chunks.push(c))
|
|
173
|
+
res.on('end', () => {
|
|
174
|
+
if (res.statusCode !== 200) {
|
|
175
|
+
reject(new Error('http ' + res.statusCode))
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')))
|
|
180
|
+
} catch (e) {
|
|
181
|
+
reject(e)
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
req.on('timeout', () => req.destroy(new Error('timeout')))
|
|
186
|
+
req.on('error', reject)
|
|
187
|
+
req.end()
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 갱신 결과를 캐시에 남긴다.
|
|
193
|
+
*
|
|
194
|
+
* **못 받았을 때도 반드시 부른다.** `armScopedRefresh` 는 "캐시가 있고 TTL 안" 일 때만 건너뛰는데,
|
|
195
|
+
* 실패했다고 아무것도 안 쓰면 캐시가 영원히 없어서 **statusLine 이 불릴 때마다**(초 단위)
|
|
196
|
+
* 새 프로세스가 뜬다 — 토큰이 없는 계정(API 키·게이트웨이)에서는 영영 성공하지 못하므로
|
|
197
|
+
* 그 상태가 계속된다 (2026-09-15 코드리뷰 지적).
|
|
198
|
+
*/
|
|
199
|
+
/**
|
|
200
|
+
* **실패해도 마지막 정상값은 지운다 — 가 아니라 지킨다.** 예전에는 실패하면 이름·퍼센트를 빈 값으로
|
|
201
|
+
* 덮어써서, 일시적 오류 한 번에 `Fable` 칸이 사라지고 실패 TTL(10분) 동안 빈 채로 있다가
|
|
202
|
+
* 다음 성공에서야 돌아왔다 (2026-09-16 실측: 10:28 실패 → 10:38 복귀, 사용자 "왜 안 나오다 또
|
|
203
|
+
* 나오나"). 몇 분 전 값이라도 빈 칸보다 낫다 — 한도는 분 단위로 크게 안 움직인다.
|
|
204
|
+
* 그래서 `scoped` 가 없으면 `prev` 의 이름·퍼센트·리셋 시각을 그대로 옮기고, 도장(`ts`)과
|
|
205
|
+
* 판정(`ok`)만 새로 찍는다. `ok:false` 는 여전히 재시도 간격을 가르는 데만 쓰인다.
|
|
206
|
+
*
|
|
207
|
+
* `err` 는 왜 비었는지 남기는 자리다 — 상태코드·타임아웃을 삼키면 이런 문제를 영영 못 찾는다.
|
|
208
|
+
*/
|
|
209
|
+
function writeScoped (scoped, prev, err) {
|
|
210
|
+
const name = scoped ? (scoped.scope?.model?.display_name ?? scoped.scope?.model?.id ?? '') : ''
|
|
211
|
+
// `resets_at` 은 여기서만 ISO 문자열이다 (stdin 쪽은 epoch 초) — 초로 맞춰 둔다
|
|
212
|
+
const resetsAt = scoped ? Math.round(Date.parse(scoped.resets_at ?? '') / 1000) || 0 : 0
|
|
213
|
+
const keep = !scoped && prev && prev.name ? prev : null
|
|
214
|
+
fs.mkdirSync(ROOT, { recursive: true })
|
|
215
|
+
const tmp = SCOPED_FILE + '.' + process.pid + '.tmp'
|
|
216
|
+
fs.writeFileSync(tmp, JSON.stringify({
|
|
217
|
+
ts: Date.now(),
|
|
218
|
+
// 다음 갱신 간격을 가르는 값 — 못 받았으면 오래 쉰다 (`armScopedRefresh`)
|
|
219
|
+
ok: !!scoped,
|
|
220
|
+
name: keep ? String(keep.name) : String(name),
|
|
221
|
+
pct: keep ? keep.pct : (scoped ? pct(scoped.percent) : null),
|
|
222
|
+
resetsAt: keep ? Number(keep.resetsAt) || 0 : resetsAt,
|
|
223
|
+
// 실패 사유 (성공이면 비운다). 값을 지킨 경우 `stale: true` 로 표시만 해 둔다
|
|
224
|
+
...(scoped ? {} : { err: String(err ?? 'unknown'), stale: !!keep }),
|
|
225
|
+
}), 'utf8')
|
|
226
|
+
fs.renameSync(tmp, SCOPED_FILE)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function refreshScoped () {
|
|
230
|
+
const prev = readScopedCache()
|
|
231
|
+
const token = readOauthToken()
|
|
232
|
+
if (!token) {
|
|
233
|
+
// 토큰이 없다 (로그인 안 함·API 키 사용) — 칸이 안 생길 뿐이지만 **도장은 찍는다**
|
|
234
|
+
writeScoped(null, prev, 'no-token')
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
let body
|
|
238
|
+
try {
|
|
239
|
+
body = await requestUsage(token)
|
|
240
|
+
} catch (e) {
|
|
241
|
+
// 네트워크·상태코드·타임아웃 — 마지막 정상값을 지키고 사유만 남긴다 (writeScoped 주석)
|
|
242
|
+
writeScoped(null, prev, e?.message || 'request-failed')
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
// 모델별 주간 한도는 여러 개일 수 있다 — 가장 많이 쓴 칸 하나만 보인다 (줄이 하나뿐이다)
|
|
246
|
+
const scoped = (Array.isArray(body?.limits) ? body.limits : [])
|
|
247
|
+
.filter(l => l?.kind === 'weekly_scoped')
|
|
248
|
+
.sort((a, b) => Number(b?.percent ?? 0) - Number(a?.percent ?? 0))[0]
|
|
249
|
+
// 모델별 한도가 없는 요금제도 있다 — 그때도 도장을 찍어야 다시 묻지 않는다.
|
|
250
|
+
// 이 경우는 실패가 아니라 "없다" 가 답이므로 옛 값을 지키지 않는다 (prev 를 안 넘긴다)
|
|
251
|
+
writeScoped(scoped ?? null, null, scoped ? undefined : 'no-scoped-limit')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** stdin JSON 에서 사이드바가 쓸 것만 뽑는다 — 원문을 통째로 두면 대화 비용·경로까지 남는다 */
|
|
255
|
+
function snapshot (data) {
|
|
256
|
+
const model = data.model ?? {}
|
|
257
|
+
const ctx = data.context_window ?? {}
|
|
258
|
+
const lim = data.rate_limits ?? {}
|
|
259
|
+
const { account, org, configDir } = readAccount()
|
|
260
|
+
const scoped = readScopedCache()
|
|
261
|
+
armScopedRefresh(scoped)
|
|
262
|
+
return {
|
|
263
|
+
sessionId: String(data.session_id ?? ''),
|
|
264
|
+
ts: Date.now(),
|
|
265
|
+
agent: 'claude',
|
|
266
|
+
model: String(model.display_name ?? model.id ?? ''),
|
|
267
|
+
modelId: String(model.id ?? ''),
|
|
268
|
+
effort: String(data.effort?.level ?? ''),
|
|
269
|
+
version: String(data.version ?? ''),
|
|
270
|
+
fastMode: !!data.fast_mode,
|
|
271
|
+
account,
|
|
272
|
+
org,
|
|
273
|
+
configDir,
|
|
274
|
+
cwd: String(data.cwd ?? data.workspace?.current_dir ?? ''),
|
|
275
|
+
contextPct: pct(ctx.used_percentage),
|
|
276
|
+
limits: {
|
|
277
|
+
fiveHourPct: pct(lim.five_hour?.used_percentage),
|
|
278
|
+
fiveHourResetsAt: Number(lim.five_hour?.resets_at) || 0,
|
|
279
|
+
sevenDayPct: pct(lim.seven_day?.used_percentage),
|
|
280
|
+
sevenDayResetsAt: Number(lim.seven_day?.resets_at) || 0,
|
|
281
|
+
scopedName: scoped?.name ?? '',
|
|
282
|
+
scopedPct: scoped?.pct ?? null,
|
|
283
|
+
scopedResetsAt: Number(scoped?.resetsAt) || 0,
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function writeSnapshot (raw) {
|
|
289
|
+
const data = JSON.parse(raw.toString('utf8'))
|
|
290
|
+
const snap = snapshot(data)
|
|
291
|
+
if (!snap.sessionId) {
|
|
292
|
+
return // 누구 것인지 모르면 쓰지 않는다 — 사이드바가 세션 id 로만 탭에 묶는다
|
|
293
|
+
}
|
|
294
|
+
fs.mkdirSync(META_DIR, { recursive: true })
|
|
295
|
+
// 같은 파일을 사이드바가 400ms 마다 읽는다. 쓰다 만 파일을 읽히지 않도록 임시 파일 + rename
|
|
296
|
+
const file = path.join(META_DIR, snap.sessionId + '.json')
|
|
297
|
+
const tmp = file + '.' + process.pid + '.tmp'
|
|
298
|
+
fs.writeFileSync(tmp, JSON.stringify(snap), 'utf8')
|
|
299
|
+
fs.renameSync(tmp, file)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** 원래 걸려 있던 statusLine 을 그대로 실행하고 stdout/stderr 을 넘겨준다 */
|
|
303
|
+
function runInner (raw) {
|
|
304
|
+
let inner = null
|
|
305
|
+
try {
|
|
306
|
+
inner = JSON.parse(fs.readFileSync(INNER_FILE, 'utf8'))
|
|
307
|
+
} catch {
|
|
308
|
+
// 안쪽이 없다 = 원래 statusLine 이 없던 사용자다. 출력 없이 끝낸다
|
|
309
|
+
}
|
|
310
|
+
const command = inner && inner.type === 'command' ? String(inner.command ?? '') : ''
|
|
311
|
+
if (!command || command.includes('agentdeck-statusline')) {
|
|
312
|
+
// 우리 자신을 안쪽으로 두면 무한히 겹친다 — 설치가 꼬였을 때의 안전판
|
|
313
|
+
process.exit(0)
|
|
314
|
+
}
|
|
315
|
+
// 겹 세기. 남의 래퍼(원래 명령을 백업해 두는 종류)가 우리를 자기 안쪽에 넣어
|
|
316
|
+
// 버리면 `우리 -> 남 -> 우리 -> ...` 고리가 생긴다. 직접 검사(위 includes)로는 한 겹밖에
|
|
317
|
+
// 못 보므로, 물려주는 env 에 깊이를 실어 두 겹을 넘으면 조용히 멈춘다. 그 화면은
|
|
318
|
+
// statusLine 이 비어 보이지만(설치를 다시 하면 풀린다) 프로세스가 무한히 늘지는 않는다
|
|
319
|
+
const depth = Number(process.env.AGENTDECK_SL_DEPTH || 0) + 1
|
|
320
|
+
if (depth > 2) {
|
|
321
|
+
process.exit(0)
|
|
322
|
+
}
|
|
323
|
+
// stdout/stderr 은 그대로 물려준다(inherit) — 우리가 중계하면 버퍼 한 겹만 늘어난다
|
|
324
|
+
const child = spawn(command, {
|
|
325
|
+
shell: true,
|
|
326
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
327
|
+
env: { ...process.env, AGENTDECK_SL_DEPTH: String(depth) },
|
|
328
|
+
})
|
|
329
|
+
child.on('error', () => process.exit(0))
|
|
330
|
+
child.on('close', code => process.exit(code ?? 0))
|
|
331
|
+
child.stdin.on('error', () => { /* 안쪽이 stdin 을 안 읽고 끝냈다 — 정상 */ })
|
|
332
|
+
child.stdin.end(raw)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 갱신 전용 실행 — 떼어 낸 프로세스가 여기로 들어온다. stdin 도 statusLine 도 건드리지 않는다
|
|
336
|
+
if (process.argv.includes('--refresh-scoped')) {
|
|
337
|
+
// exit 을 부르지 않는다 — 응답이 끝나면 루프가 비어 저절로 끝난다 (requestUsage 주석)
|
|
338
|
+
refreshScoped().catch(() => {
|
|
339
|
+
// 네트워크·토큰 문제. **여기서도 도장을 찍는다** — 안 찍으면 TTL 게이트가 비어
|
|
340
|
+
// 렌더마다 새 프로세스가 뜬다 (writeScoped 주석)
|
|
341
|
+
try { writeScoped(null, readScopedCache(), 'unhandled') } catch { /* 디스크까지 막혔으면 할 수 있는 게 없다 */ }
|
|
342
|
+
})
|
|
343
|
+
} else {
|
|
344
|
+
const raw = readStdin()
|
|
345
|
+
try {
|
|
346
|
+
writeSnapshot(raw)
|
|
347
|
+
} catch {
|
|
348
|
+
// 베끼기 실패는 사이드바 한 줄이 안 뜨는 것으로 끝난다. statusLine 은 계속 나가야 한다
|
|
349
|
+
}
|
|
350
|
+
runInner(raw)
|
|
351
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tabby-agentdeck",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Run AI coding agents side by side in Tabby - a live deck of agent sessions with status, a preview panel for the files they touch, agent-friendly hotkeys and image paste",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tabby-plugin",
|
|
@@ -14,17 +14,25 @@
|
|
|
14
14
|
"main": "dist/index.js",
|
|
15
15
|
"typings": "dist/index.d.ts",
|
|
16
16
|
"scripts": {
|
|
17
|
-
"build": "webpack --
|
|
17
|
+
"build": "webpack --mode production",
|
|
18
|
+
"build:release": "node tools/sync-version.js && webpack --mode production --env release",
|
|
19
|
+
"prepack": "npm run build:release && node tools/check-release.js",
|
|
18
20
|
"watch": "webpack --progress --color --watch",
|
|
19
|
-
"test": "
|
|
21
|
+
"test": "npm run test:existing && npm run test:codex",
|
|
20
22
|
"prebuild": "node tools/sync-version.js",
|
|
21
|
-
"sync-version": "node tools/sync-version.js"
|
|
23
|
+
"sync-version": "node tools/sync-version.js",
|
|
24
|
+
"release": "node tools/release.js",
|
|
25
|
+
"test:existing": "tsc src/codexHooks.ts src/i18n.ts src/prompt.ts src/detect.ts src/claudeHooks.ts src/screen.ts src/bind.ts src/order.ts src/reason.ts src/tabenv.ts src/viewer.ts src/markdown.ts src/ime.ts src/gitDiff.ts src/agents.ts src/group.ts src/project-root.ts src/gitCommit.ts src/diag.ts src/subagents.ts src/nav.ts src/keybind.ts src/sessionLedger.ts src/update.ts src/api.ts src/dock.ts src/status.service.ts src/profile.service.ts src/recovery.service.ts src/meta.ts src/statusLine.ts src/codexMeta.ts --outDir .tmp --module commonjs --target es2019 --skipLibCheck --experimentalDecorators --esModuleInterop && node test/version.test.js && node test/tags.test.js && node test/prompt.test.js && node test/detect.test.js && node test/claudeHooks.test.js && node test/screen.test.js && node test/bind.test.js && node test/order.test.js && node test/reason.test.js && node test/tabenv.test.js && node test/viewer.test.js && node test/markdown.test.js && node test/ime.test.js && node test/gitDiff.test.js && node test/agents.test.js && node test/group.test.js && node test/gitCommit.test.js && node test/diag.test.js && node test/subagents.test.js && node test/nav.test.js && node test/sessionLedger.test.js && node test/update.test.js && node test/api.test.js && node test/dock.test.js && node test/status.service.test.js && node test/profile.service.test.js && node test/recovery.service.test.js && node test/meta.test.js && node test/i18n.test.js && node test/statusLine.test.js && node test/hotkeys.test.js && node test/keybind.test.js && node test/codexMeta.test.js && node test/codexHooks.test.js",
|
|
26
|
+
"test:codex": "node test/regression.cjs && node test/resume.cjs && node test/cursor.cjs && node test/wheel.cjs",
|
|
27
|
+
"typecheck": "tsc --noEmit"
|
|
22
28
|
},
|
|
23
29
|
"files": [
|
|
24
30
|
"dist",
|
|
25
31
|
"hooks",
|
|
26
32
|
"updater",
|
|
27
|
-
"install.ps1"
|
|
33
|
+
"install.ps1",
|
|
34
|
+
"CHANGELOG.md",
|
|
35
|
+
"README.ko.md"
|
|
28
36
|
],
|
|
29
37
|
"license": "MIT",
|
|
30
38
|
"devDependencies": {
|
|
@@ -33,6 +41,7 @@
|
|
|
33
41
|
"@angular/forms": "^15.2.6",
|
|
34
42
|
"@types/node": "^20.19.43",
|
|
35
43
|
"@types/webpack-env": "^1.18.4",
|
|
44
|
+
"@xterm/headless": "^5.5.0",
|
|
36
45
|
"css-loader": "^6.10.0",
|
|
37
46
|
"rxjs": "^7.5.7",
|
|
38
47
|
"sass": "^1.71.1",
|
|
@@ -49,5 +58,13 @@
|
|
|
49
58
|
},
|
|
50
59
|
"author": {
|
|
51
60
|
"name": "junggon"
|
|
61
|
+
},
|
|
62
|
+
"repository": {
|
|
63
|
+
"type": "git",
|
|
64
|
+
"url": "git+https://github.com/jghwang2/tabby-agentdeck.git"
|
|
65
|
+
},
|
|
66
|
+
"homepage": "https://jghwang2.github.io/tabby-agentdeck/",
|
|
67
|
+
"bugs": {
|
|
68
|
+
"url": "https://github.com/jghwang2/tabby-agentdeck/issues"
|
|
52
69
|
}
|
|
53
70
|
}
|