@cocorograph/hub-agent 0.6.22 → 0.6.25
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/package.json +1 -1
- package/src/claude-upload.mjs +226 -0
- package/src/main.mjs +94 -7
- package/src/usage.mjs +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cockpit チャットモードの添付ファイル受信 (browser → agent) を司るモジュール。
|
|
3
|
+
*
|
|
4
|
+
* 設計の背景 (WHY):
|
|
5
|
+
* - Claude が実際に動くのは hub-agent が常駐するローカルマシンで、Hub バックエンド
|
|
6
|
+
* とは別ホスト。添付ファイルのバイト列の最終着地点はこのローカル FS であり、
|
|
7
|
+
* そこへの転送経路は WS トンネル (claude.* チャネル) しか無い。
|
|
8
|
+
* - そこで「ファイルを cwd 配下に保存 → 保存パスを browser に返却 → browser は
|
|
9
|
+
* claude.input 本文にパス参照を載せる → Claude が Read ツールで読む」という
|
|
10
|
+
* パス参照方式を採る。Read ツールは画像 / PDF / ipynb 等をネイティブ対応する
|
|
11
|
+
* ため、対応形式を Claude の Read 対応形式に自然に揃えられる。
|
|
12
|
+
* - 大きいファイルで単一 WS フレームが肥大しないよう、browser 側でチャンク分割
|
|
13
|
+
* して送る。本モジュールは upload_id 単位でチャンクをメモリに蓄積し、全チャンク
|
|
14
|
+
* 到着時に結合して 1 ファイルとして書き出す。
|
|
15
|
+
*
|
|
16
|
+
* セキュリティ / 安全策:
|
|
17
|
+
* - 保存先は cwd 配下の `.cockpit-uploads/<日付>/` 固定。ファイル名は basename 化 +
|
|
18
|
+
* パス区切り / 制御文字を除去し、ディレクトリトラバーサルを防ぐ (日本語名は保持)。
|
|
19
|
+
* - 総サイズ上限 (既定 50MB) と最大チャンク数でメモリ枯渇 / DoS を防ぐ。
|
|
20
|
+
* - 未完了アップロードは TTL 経過で破棄する (sweep)。
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { mkdir, writeFile } from "node:fs/promises"
|
|
24
|
+
import os from "node:os"
|
|
25
|
+
import path from "node:path"
|
|
26
|
+
|
|
27
|
+
/** 保存先のサブディレクトリ名 (cwd 直下に作る)。 */
|
|
28
|
+
export const UPLOAD_DIRNAME = ".cockpit-uploads"
|
|
29
|
+
|
|
30
|
+
/** 1 ファイルあたりの総サイズ上限 (バイト)。既定 50MB。 */
|
|
31
|
+
const DEFAULT_MAX_TOTAL_BYTES = 50 * 1024 * 1024
|
|
32
|
+
|
|
33
|
+
/** 1 アップロードあたりの最大チャンク数 (異常値ガード)。 */
|
|
34
|
+
const MAX_CHUNKS = 8192
|
|
35
|
+
|
|
36
|
+
/** 未完了アップロードの保持上限 (ミリ秒)。これを超えたら破棄する。 */
|
|
37
|
+
const DEFAULT_UPLOAD_TTL_MS = 30 * 60 * 1000
|
|
38
|
+
|
|
39
|
+
/** 衝突回避用のプロセス内連番。Date.now() と併用してユニーク名を作る。 */
|
|
40
|
+
let _seq = 0
|
|
41
|
+
|
|
42
|
+
/** 除去対象の文字: 制御文字 (0x00-0x1f) + パス区切り + Windows 予約文字。 */
|
|
43
|
+
// eslint-disable-next-line no-control-regex
|
|
44
|
+
const UNSAFE_NAME_CHARS = new RegExp("[\\u0000-\\u001f<>:\"/\\\\|?*]", "g")
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* ファイル名をローカル FS に安全な basename へ正規化する。
|
|
48
|
+
*
|
|
49
|
+
* - ディレクトリ成分は basename で除去 (`../` トラバーサル対策)
|
|
50
|
+
* - パス区切り / Windows 予約文字 / 制御文字を `_` へ置換
|
|
51
|
+
* - 先頭ドット (隠しファイル化) を除去
|
|
52
|
+
* - Unicode 文字は保持する (日本語ファイル名を壊さない)
|
|
53
|
+
*
|
|
54
|
+
* @param {unknown} name 元のファイル名
|
|
55
|
+
* @returns {string} 安全な basename (空になる場合は "file")
|
|
56
|
+
*/
|
|
57
|
+
export function sanitizeFilename(name) {
|
|
58
|
+
let base = path.basename(String(name == null ? "" : name))
|
|
59
|
+
base = base.replace(UNSAFE_NAME_CHARS, "_").replace(/^\.+/, "").trim()
|
|
60
|
+
base = base.slice(0, 150)
|
|
61
|
+
return base || "file"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** `YYYY-MM-DD` 形式の日付文字列を返す (ローカルタイム)。 */
|
|
65
|
+
function todayDir() {
|
|
66
|
+
const d = new Date()
|
|
67
|
+
const pad = (n) => String(n).padStart(2, "0")
|
|
68
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 1 stream に紐付かない、プロセス全体で 1 つのアップロード受信器。
|
|
73
|
+
* upload_id 単位で進行中アップロードを保持する。
|
|
74
|
+
*/
|
|
75
|
+
export class UploadManager {
|
|
76
|
+
/**
|
|
77
|
+
* @param {object} opts
|
|
78
|
+
* @param {object} [opts.logger] pino 互換ロガー
|
|
79
|
+
* @param {number} [opts.maxTotalBytes] 1 ファイルの総サイズ上限
|
|
80
|
+
* @param {number} [opts.ttlMs] 未完了アップロードの保持上限 (ミリ秒)
|
|
81
|
+
*/
|
|
82
|
+
constructor({ logger, maxTotalBytes = DEFAULT_MAX_TOTAL_BYTES, ttlMs = DEFAULT_UPLOAD_TTL_MS } = {}) {
|
|
83
|
+
this.logger = logger
|
|
84
|
+
this.maxTotalBytes = maxTotalBytes
|
|
85
|
+
this.ttlMs = ttlMs
|
|
86
|
+
/** @type {Map<string, {filename:string, media_type:string, total_size:number, total_chunks:number, chunks:Buffer[], received:number, bytes:number, cwd:(string|undefined), createdAt:number}>} */
|
|
87
|
+
this._pending = new Map()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 保存先のベースディレクトリ (cwd 優先、無ければ HOME)。 */
|
|
91
|
+
_resolveBaseDir(cwd) {
|
|
92
|
+
return cwd && typeof cwd === "string" ? cwd : os.homedir()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* チャンク 1 件を受理する。全チャンク到着で結合 + ファイル書き込みまで行い、
|
|
97
|
+
* `{ done: true, path, size, filename, media_type }` を返す。継続中は
|
|
98
|
+
* `{ done: false }` を返す。
|
|
99
|
+
*
|
|
100
|
+
* @param {object} chunk
|
|
101
|
+
* @param {string} chunk.upload_id アップロードの一意 ID (browser 採番)
|
|
102
|
+
* @param {string} [chunk.filename] 元ファイル名
|
|
103
|
+
* @param {string} [chunk.media_type] MIME タイプ
|
|
104
|
+
* @param {number} chunk.total_size 総バイト数 (検証用)
|
|
105
|
+
* @param {number} chunk.total_chunks 総チャンク数
|
|
106
|
+
* @param {number} chunk.chunk_index このチャンクの index (0 始まり)
|
|
107
|
+
* @param {string} chunk.data base64 エンコードされたチャンク本体
|
|
108
|
+
* @param {string} [chunk.cwd] 保存先解決に使う cwd
|
|
109
|
+
* @returns {Promise<{done:boolean, path?:string, size?:number, filename?:string, media_type?:string}>}
|
|
110
|
+
* @throws {Error} バリデーション失敗 / サイズ超過時
|
|
111
|
+
*/
|
|
112
|
+
async addChunk({
|
|
113
|
+
upload_id,
|
|
114
|
+
filename,
|
|
115
|
+
media_type,
|
|
116
|
+
total_size,
|
|
117
|
+
total_chunks,
|
|
118
|
+
chunk_index,
|
|
119
|
+
data,
|
|
120
|
+
cwd,
|
|
121
|
+
}) {
|
|
122
|
+
if (!upload_id || typeof upload_id !== "string") {
|
|
123
|
+
throw new Error("upload_id required")
|
|
124
|
+
}
|
|
125
|
+
let rec = this._pending.get(upload_id)
|
|
126
|
+
if (!rec) {
|
|
127
|
+
const tc = Number(total_chunks)
|
|
128
|
+
const ts = Number(total_size)
|
|
129
|
+
if (!Number.isInteger(tc) || tc < 1 || tc > MAX_CHUNKS) {
|
|
130
|
+
throw new Error("invalid total_chunks")
|
|
131
|
+
}
|
|
132
|
+
if (!Number.isInteger(ts) || ts < 0 || ts > this.maxTotalBytes) {
|
|
133
|
+
throw new Error(`file too large (max ${this.maxTotalBytes} bytes)`)
|
|
134
|
+
}
|
|
135
|
+
rec = {
|
|
136
|
+
filename,
|
|
137
|
+
media_type,
|
|
138
|
+
total_size: ts,
|
|
139
|
+
total_chunks: tc,
|
|
140
|
+
chunks: new Array(tc),
|
|
141
|
+
received: 0,
|
|
142
|
+
bytes: 0,
|
|
143
|
+
cwd,
|
|
144
|
+
createdAt: Date.now(),
|
|
145
|
+
}
|
|
146
|
+
this._pending.set(upload_id, rec)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const idx = Number(chunk_index)
|
|
150
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= rec.total_chunks) {
|
|
151
|
+
throw new Error("invalid chunk_index")
|
|
152
|
+
}
|
|
153
|
+
if (rec.chunks[idx] !== undefined) {
|
|
154
|
+
// 重複チャンク (再送等) は冪等に無視する。
|
|
155
|
+
return { done: false }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const buf = Buffer.from(typeof data === "string" ? data : "", "base64")
|
|
159
|
+
rec.bytes += buf.length
|
|
160
|
+
if (rec.bytes > this.maxTotalBytes) {
|
|
161
|
+
this._pending.delete(upload_id)
|
|
162
|
+
throw new Error(`file too large (max ${this.maxTotalBytes} bytes)`)
|
|
163
|
+
}
|
|
164
|
+
rec.chunks[idx] = buf
|
|
165
|
+
rec.received += 1
|
|
166
|
+
|
|
167
|
+
if (rec.received < rec.total_chunks) {
|
|
168
|
+
return { done: false }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 全チャンク到着 → 結合して書き込み。
|
|
172
|
+
this._pending.delete(upload_id)
|
|
173
|
+
const full = Buffer.concat(rec.chunks)
|
|
174
|
+
const savePath = await this._writeFile(rec, full)
|
|
175
|
+
this.logger?.info?.(
|
|
176
|
+
{ upload_id, path: savePath, size: full.length, filename: rec.filename },
|
|
177
|
+
"cockpit upload saved",
|
|
178
|
+
)
|
|
179
|
+
return {
|
|
180
|
+
done: true,
|
|
181
|
+
path: savePath,
|
|
182
|
+
size: full.length,
|
|
183
|
+
filename: rec.filename,
|
|
184
|
+
media_type: rec.media_type,
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** 結合済みバッファを cwd 配下に書き出し、絶対パスを返す。 */
|
|
189
|
+
async _writeFile(rec, buf) {
|
|
190
|
+
const baseDir = this._resolveBaseDir(rec.cwd)
|
|
191
|
+
const dir = path.join(baseDir, UPLOAD_DIRNAME, todayDir())
|
|
192
|
+
await mkdir(dir, { recursive: true })
|
|
193
|
+
const safe = sanitizeFilename(rec.filename)
|
|
194
|
+
_seq = (_seq + 1) % 1_000_000
|
|
195
|
+
const unique = `${Date.now().toString(36)}-${_seq.toString(36)}`
|
|
196
|
+
const finalName = `${unique}_${safe}`
|
|
197
|
+
const full = path.join(dir, finalName)
|
|
198
|
+
await writeFile(full, buf)
|
|
199
|
+
return full
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* TTL を超過した未完了アップロードを破棄する (メモリ保護)。
|
|
204
|
+
* 定期タイマー or 受信契機で呼ぶ想定。
|
|
205
|
+
* @param {number} [now] 現在時刻 (テスト用に注入可能)
|
|
206
|
+
* @returns {number} 破棄した件数
|
|
207
|
+
*/
|
|
208
|
+
sweep(now = Date.now()) {
|
|
209
|
+
let dropped = 0
|
|
210
|
+
for (const [id, rec] of this._pending) {
|
|
211
|
+
if (now - rec.createdAt > this.ttlMs) {
|
|
212
|
+
this._pending.delete(id)
|
|
213
|
+
dropped += 1
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (dropped > 0) {
|
|
217
|
+
this.logger?.info?.({ dropped }, "cockpit upload: swept stale uploads")
|
|
218
|
+
}
|
|
219
|
+
return dropped
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** 進行中アップロード件数 (テスト / 監視用)。 */
|
|
223
|
+
get pendingCount() {
|
|
224
|
+
return this._pending.size
|
|
225
|
+
}
|
|
226
|
+
}
|
package/src/main.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import { loadPlugins, runHookBroadcast, runHookChain } from "./plugin-loader.mjs
|
|
|
22
22
|
import { WsClient } from "./ws-client.mjs"
|
|
23
23
|
import { PtyBridge } from "./pty-bridge.mjs"
|
|
24
24
|
import { ClaudeStreamBridge } from "./claude-stream-bridge.mjs"
|
|
25
|
+
import { UploadManager } from "./claude-upload.mjs"
|
|
25
26
|
import { fetchSessionHistory, listSessions } from "./claude-history.mjs"
|
|
26
27
|
import { listAgents } from "./agents.mjs"
|
|
27
28
|
import { listSkills } from "./skills.mjs"
|
|
@@ -37,7 +38,12 @@ import {
|
|
|
37
38
|
listWorktreeStubs,
|
|
38
39
|
removeWorktree as removeWorktreeDir,
|
|
39
40
|
} from "./tmux.mjs"
|
|
40
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
contextWindowSize,
|
|
43
|
+
getSessionUsages,
|
|
44
|
+
getUsage,
|
|
45
|
+
recordChatRateLimit,
|
|
46
|
+
} from "./usage.mjs"
|
|
41
47
|
import { getChatSignal, recordChatActivity } from "./chat-signals.mjs"
|
|
42
48
|
|
|
43
49
|
const logger = pino({ name: "hub-agent" })
|
|
@@ -149,6 +155,11 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
149
155
|
? new ClaudeStreamBridge({ sdk: resolvedSdk, logger })
|
|
150
156
|
: null
|
|
151
157
|
|
|
158
|
+
// Cockpit チャットモードの添付ファイル受信器 (browser → agent のチャンク送信を
|
|
159
|
+
// ローカル FS に保存し、保存パスを返す)。SDK 有無に関わらず生成してよいが、添付は
|
|
160
|
+
// チャットモード専用機能なので claudeBridge と同じく stream モード前提で使う。
|
|
161
|
+
const uploadManager = new UploadManager({ logger })
|
|
162
|
+
|
|
152
163
|
const bundleVersion = await readBundleVersion()
|
|
153
164
|
if (bundleVersion) {
|
|
154
165
|
logger.info({ bundleVersion }, "hub bundle version detected")
|
|
@@ -252,7 +263,7 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
252
263
|
let dispatchChain = Promise.resolve()
|
|
253
264
|
client.on("message", (msg) => {
|
|
254
265
|
dispatchChain = dispatchChain
|
|
255
|
-
.then(() => dispatch(msg, { ...ctx, client, ptyBridge, claudeBridge }))
|
|
266
|
+
.then(() => dispatch(msg, { ...ctx, client, ptyBridge, claudeBridge, uploadManager }))
|
|
256
267
|
.catch((err) => {
|
|
257
268
|
logger.error(
|
|
258
269
|
{ err: err.message, type: msg?.type },
|
|
@@ -313,21 +324,44 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
313
324
|
)
|
|
314
325
|
})
|
|
315
326
|
|
|
316
|
-
return { client, plugins, ptyBridge, claudeBridge }
|
|
327
|
+
return { client, plugins, ptyBridge, claudeBridge, uploadManager }
|
|
317
328
|
}
|
|
318
329
|
|
|
319
330
|
const SESSION_EVENTS_DIR =
|
|
320
331
|
process.env.COCKPIT_SESSION_EVENTS_DIR || "/tmp/cockpit_session_events"
|
|
321
332
|
|
|
322
|
-
// context 窓サイズ (トークン)
|
|
323
|
-
|
|
333
|
+
// context 窓サイズ (トークン) の既定。env HUB_CONTEXT_WINDOW があれば最優先 (手動上書き)。
|
|
334
|
+
// 無ければ起動後に statusLine cache の context_window_size (Opus 4.8 の 1M 等) を
|
|
335
|
+
// refreshContextWindow() で解決して resolvedContextWindow を上書きする。
|
|
336
|
+
const CONTEXT_WINDOW_FALLBACK = Number(process.env.HUB_CONTEXT_WINDOW) || 200000
|
|
337
|
+
|
|
338
|
+
// 実際に context% 計算で使う窓サイズ。startStateLoop の tick で定期リフレッシュされる。
|
|
339
|
+
// 200k 固定だと 1M ベータのセッションで分母が 5 倍小さく、ドーナツが常に振り切れる
|
|
340
|
+
// (273k トークン → 137% を 100% にクランプ表示) 不具合があったため、実窓サイズを使う。
|
|
341
|
+
let resolvedContextWindow = CONTEXT_WINDOW_FALLBACK
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* 実コンテキスト窓サイズ (1M ベータ等) を statusLine cache から解決して
|
|
345
|
+
* resolvedContextWindow を更新する。env HUB_CONTEXT_WINDOW 指定時は手動上書きを
|
|
346
|
+
* 尊重して何もしない。ファイル読み失敗時は前回値を保持する。
|
|
347
|
+
*/
|
|
348
|
+
export async function refreshContextWindow() {
|
|
349
|
+
if (process.env.HUB_CONTEXT_WINDOW) return // 手動上書きが最優先
|
|
350
|
+
try {
|
|
351
|
+
const size = await contextWindowSize()
|
|
352
|
+
if (typeof size === "number" && size > 0) resolvedContextWindow = size
|
|
353
|
+
} catch {
|
|
354
|
+
/* 前回値を保持 */
|
|
355
|
+
}
|
|
356
|
+
}
|
|
324
357
|
|
|
325
358
|
/**
|
|
326
359
|
* SDK assistant メッセージの usage から context 使用率 (%) を概算する。
|
|
327
360
|
* context = input + cache_read + cache_creation + output(直近応答)。
|
|
328
361
|
* プロンプトキャッシュ有効時は cache_* に大半が乗るため 4 種を合算する。
|
|
362
|
+
* 分母は resolvedContextWindow (実窓サイズ。1M ベータ等を自動反映)。
|
|
329
363
|
*/
|
|
330
|
-
function contextPctFromUsage(u) {
|
|
364
|
+
export function contextPctFromUsage(u) {
|
|
331
365
|
if (!u) return null
|
|
332
366
|
const tokens =
|
|
333
367
|
(u.input_tokens || 0) +
|
|
@@ -335,7 +369,7 @@ function contextPctFromUsage(u) {
|
|
|
335
369
|
(u.cache_creation_input_tokens || 0) +
|
|
336
370
|
(u.output_tokens || 0)
|
|
337
371
|
if (tokens <= 0) return null
|
|
338
|
-
return Math.min(100, Math.round((tokens /
|
|
372
|
+
return Math.min(100, Math.round((tokens / resolvedContextWindow) * 1000) / 10)
|
|
339
373
|
}
|
|
340
374
|
|
|
341
375
|
/**
|
|
@@ -483,6 +517,9 @@ function startStateLoop({ client, plugins, logger, intervalMs }) {
|
|
|
483
517
|
const tick = async () => {
|
|
484
518
|
if (stopped) return
|
|
485
519
|
try {
|
|
520
|
+
// 実コンテキスト窓サイズ (1M ベータ等) を反映。contextPctFromUsage の分母を
|
|
521
|
+
// 5s ごとに最新化し、ドーナツが 200k 固定で振り切れるのを防ぐ。
|
|
522
|
+
await refreshContextWindow()
|
|
486
523
|
const states = await listSessionStates({ plugins, logger })
|
|
487
524
|
for (const s of states) {
|
|
488
525
|
let status = s.status
|
|
@@ -743,6 +780,56 @@ async function dispatch(msg, ctx) {
|
|
|
743
780
|
message: msg.message,
|
|
744
781
|
})
|
|
745
782
|
return
|
|
783
|
+
case "claude.upload": {
|
|
784
|
+
// Cockpit チャットモードの添付ファイル受信 (チャンク 1 件)。全チャンク到着で
|
|
785
|
+
// cwd 配下の .cockpit-uploads/<日付>/ に保存し、保存パスを browser へ返す。
|
|
786
|
+
// browser はそのパスを claude.input 本文に載せ、Claude が Read ツールで読む。
|
|
787
|
+
if (!ctx.uploadManager) {
|
|
788
|
+
ctx.client.send({
|
|
789
|
+
type: "claude.upload.done",
|
|
790
|
+
stream_id: msg.stream_id,
|
|
791
|
+
upload_id: msg.upload_id,
|
|
792
|
+
ok: false,
|
|
793
|
+
error: "upload_unavailable",
|
|
794
|
+
})
|
|
795
|
+
return
|
|
796
|
+
}
|
|
797
|
+
try {
|
|
798
|
+
const res = await ctx.uploadManager.addChunk({
|
|
799
|
+
upload_id: msg.upload_id,
|
|
800
|
+
filename: msg.filename,
|
|
801
|
+
media_type: msg.media_type,
|
|
802
|
+
total_size: msg.total_size,
|
|
803
|
+
total_chunks: msg.total_chunks,
|
|
804
|
+
chunk_index: msg.chunk_index,
|
|
805
|
+
data: msg.data,
|
|
806
|
+
cwd: msg.cwd || ctx.config?.claude_cwd || undefined,
|
|
807
|
+
})
|
|
808
|
+
if (res.done) {
|
|
809
|
+
// 完了チャンクのみ done を返す。継続中チャンクは応答しない
|
|
810
|
+
// (browser は自分のチャンク送出完了で進捗を把握する)。
|
|
811
|
+
ctx.client.send({
|
|
812
|
+
type: "claude.upload.done",
|
|
813
|
+
stream_id: msg.stream_id,
|
|
814
|
+
upload_id: msg.upload_id,
|
|
815
|
+
ok: true,
|
|
816
|
+
path: res.path,
|
|
817
|
+
filename: res.filename,
|
|
818
|
+
media_type: res.media_type,
|
|
819
|
+
size: res.size,
|
|
820
|
+
})
|
|
821
|
+
}
|
|
822
|
+
} catch (err) {
|
|
823
|
+
ctx.client.send({
|
|
824
|
+
type: "claude.upload.done",
|
|
825
|
+
stream_id: msg.stream_id,
|
|
826
|
+
upload_id: msg.upload_id,
|
|
827
|
+
ok: false,
|
|
828
|
+
error: err?.message || String(err),
|
|
829
|
+
})
|
|
830
|
+
}
|
|
831
|
+
return
|
|
832
|
+
}
|
|
746
833
|
case "claude.permission.reply":
|
|
747
834
|
if (!ctx.claudeBridge) return
|
|
748
835
|
ctx.claudeBridge.permissionReply({
|
package/src/usage.mjs
CHANGED
|
@@ -360,7 +360,7 @@ async function statuslineCacheMtime() {
|
|
|
360
360
|
* あればそれを使う (1M ベータ等を自動反映)。無ければ HUB_CONTEXT_WINDOW / 200000。
|
|
361
361
|
* ※ cache が stale でも窓サイズ自体は不変なので利用できる。
|
|
362
362
|
*/
|
|
363
|
-
async function contextWindowSize() {
|
|
363
|
+
export async function contextWindowSize() {
|
|
364
364
|
const text = await readOrNull(statuslineCache())
|
|
365
365
|
if (text) {
|
|
366
366
|
try {
|