@vacbo/opencode-anthropic-fix 0.1.7 → 0.1.9
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 +88 -88
- package/dist/opencode-anthropic-auth-cli.mjs +804 -507
- package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
- package/package.json +67 -59
- package/src/__tests__/billing-edge-cases.test.ts +59 -59
- package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
- package/src/__tests__/cc-comparison.test.ts +87 -87
- package/src/__tests__/cc-credentials.test.ts +254 -250
- package/src/__tests__/cch-drift-checker.test.ts +51 -51
- package/src/__tests__/cch-native-style.test.ts +56 -56
- package/src/__tests__/debug-gating.test.ts +42 -42
- package/src/__tests__/decomposition-smoke.test.ts +68 -68
- package/src/__tests__/fingerprint-regression.test.ts +575 -566
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
- package/src/__tests__/helpers/conversation-history.ts +119 -119
- package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
- package/src/__tests__/helpers/deferred.ts +69 -69
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
- package/src/__tests__/helpers/in-memory-storage.ts +88 -88
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
- package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
- package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
- package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
- package/src/__tests__/helpers/sse.ts +209 -209
- package/src/__tests__/index.parallel.test.ts +605 -595
- package/src/__tests__/sanitization-regex.test.ts +112 -112
- package/src/__tests__/state-bounds.test.ts +90 -90
- package/src/account-identity.test.ts +197 -192
- package/src/account-identity.ts +69 -67
- package/src/account-state.test.ts +86 -86
- package/src/account-state.ts +25 -25
- package/src/accounts/matching.test.ts +335 -0
- package/src/accounts/matching.ts +167 -0
- package/src/accounts/persistence.test.ts +345 -0
- package/src/accounts/persistence.ts +432 -0
- package/src/accounts/repair.test.ts +276 -0
- package/src/accounts/repair.ts +407 -0
- package/src/accounts.dedup.test.ts +621 -621
- package/src/accounts.test.ts +933 -929
- package/src/accounts.ts +633 -989
- package/src/backoff.test.ts +345 -345
- package/src/backoff.ts +219 -219
- package/src/betas.ts +124 -124
- package/src/bun-fetch.test.ts +345 -342
- package/src/bun-fetch.ts +424 -424
- package/src/bun-proxy.test.ts +25 -25
- package/src/bun-proxy.ts +209 -209
- package/src/cc-credentials.ts +111 -111
- package/src/circuit-breaker.test.ts +184 -184
- package/src/circuit-breaker.ts +169 -169
- package/src/cli/commands/auth.ts +963 -0
- package/src/cli/commands/config.ts +547 -0
- package/src/cli/formatting.test.ts +406 -0
- package/src/cli/formatting.ts +219 -0
- package/src/cli.ts +255 -2022
- package/src/commands/handlers/betas.ts +100 -0
- package/src/commands/handlers/config.ts +99 -0
- package/src/commands/handlers/files.ts +375 -0
- package/src/commands/oauth-flow.ts +181 -166
- package/src/commands/prompts.ts +61 -61
- package/src/commands/router.test.ts +421 -0
- package/src/commands/router.ts +143 -635
- package/src/config.test.ts +482 -482
- package/src/config.ts +412 -404
- package/src/constants.ts +48 -48
- package/src/drift/cch-constants.ts +95 -95
- package/src/env.ts +111 -105
- package/src/headers/billing.ts +33 -33
- package/src/headers/builder.ts +130 -130
- package/src/headers/cch.ts +75 -75
- package/src/headers/stainless.ts +25 -25
- package/src/headers/user-agent.ts +23 -23
- package/src/index.ts +436 -828
- package/src/models.ts +27 -27
- package/src/oauth.test.ts +102 -102
- package/src/oauth.ts +178 -178
- package/src/parent-pid-watcher.test.ts +148 -148
- package/src/parent-pid-watcher.ts +69 -69
- package/src/plugin-helpers.ts +82 -82
- package/src/refresh-helpers.ts +145 -139
- package/src/refresh-lock.test.ts +94 -94
- package/src/refresh-lock.ts +93 -93
- package/src/request/body.history.test.ts +579 -571
- package/src/request/body.ts +255 -255
- package/src/request/metadata.ts +65 -65
- package/src/request/retry.test.ts +156 -156
- package/src/request/retry.ts +67 -67
- package/src/request/url.ts +21 -21
- package/src/request-orchestration-helpers.ts +648 -0
- package/src/response/index.ts +5 -5
- package/src/response/mcp.ts +58 -58
- package/src/response/streaming.test.ts +313 -311
- package/src/response/streaming.ts +412 -410
- package/src/rotation.test.ts +304 -301
- package/src/rotation.ts +205 -205
- package/src/storage.test.ts +547 -547
- package/src/storage.ts +315 -291
- package/src/system-prompt/builder.ts +38 -38
- package/src/system-prompt/index.ts +5 -5
- package/src/system-prompt/normalize.ts +60 -60
- package/src/system-prompt/sanitize.ts +30 -30
- package/src/thinking.ts +21 -20
- package/src/token-refresh.test.ts +265 -265
- package/src/token-refresh.ts +219 -214
- package/src/types.ts +30 -30
- package/dist/bun-proxy.mjs +0 -291
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Betas slash-command handler (/anthropic betas)
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import { resolveBetaShortcut } from "../../betas.js";
|
|
6
|
+
import type { AnthropicAuthConfig } from "../../config.js";
|
|
7
|
+
import { loadConfigFresh, saveConfig } from "../../config.js";
|
|
8
|
+
import { isTruthyEnv } from "../../env.js";
|
|
9
|
+
|
|
10
|
+
export interface BetasHandlerDeps {
|
|
11
|
+
sendCommandMessage: (sessionID: string, message: string) => Promise<void>;
|
|
12
|
+
config: AnthropicAuthConfig;
|
|
13
|
+
initialAccountPinned: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Handle /anthropic betas [list|add|remove <beta>].
|
|
18
|
+
*/
|
|
19
|
+
export async function handleBetasCommand(sessionID: string, args: string[], deps: BetasHandlerDeps): Promise<void> {
|
|
20
|
+
const { sendCommandMessage, config, initialAccountPinned } = deps;
|
|
21
|
+
const action = (args[1] || "").toLowerCase();
|
|
22
|
+
|
|
23
|
+
if (!action || action === "list") {
|
|
24
|
+
const fresh = loadConfigFresh();
|
|
25
|
+
const strategy = fresh.account_selection_strategy || config.account_selection_strategy;
|
|
26
|
+
const lines = [
|
|
27
|
+
"▣ Anthropic Betas",
|
|
28
|
+
"",
|
|
29
|
+
"Preset betas (auto-computed per model/provider):",
|
|
30
|
+
" oauth-2025-04-20, claude-code-20250219,",
|
|
31
|
+
" advanced-tool-use-2025-11-20, fast-mode-2026-02-01,",
|
|
32
|
+
" interleaved-thinking-2025-05-14 (non-Opus 4.6) OR effort-2025-11-24 (Opus 4.6),",
|
|
33
|
+
" files-api-2025-04-14 (only /v1/files and requests with file_id),",
|
|
34
|
+
" token-counting-2024-11-01 (only /v1/messages/count_tokens),",
|
|
35
|
+
` prompt-caching-scope-2026-01-05 (non-interactive${strategy === "round-robin" ? ", skipped in round-robin" : ""})`,
|
|
36
|
+
"",
|
|
37
|
+
`Experimental betas: ${isTruthyEnv(process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS) ? "disabled (CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1)" : "enabled"}`,
|
|
38
|
+
`Strategy: ${strategy}${initialAccountPinned ? " (pinned via OPENCODE_ANTHROPIC_INITIAL_ACCOUNT)" : ""}`,
|
|
39
|
+
`Custom betas: ${fresh.custom_betas.length ? fresh.custom_betas.join(", ") : "(none)"}`,
|
|
40
|
+
"",
|
|
41
|
+
"Toggleable presets:",
|
|
42
|
+
" /anthropic betas add structured-outputs-2025-12-15",
|
|
43
|
+
" /anthropic betas add context-management-2025-06-27",
|
|
44
|
+
" /anthropic betas add task-budgets-2026-03-13",
|
|
45
|
+
" /anthropic betas add web-search-2025-03-05",
|
|
46
|
+
" /anthropic betas add compact-2026-01-12",
|
|
47
|
+
" /anthropic betas add mcp-servers-2025-12-04",
|
|
48
|
+
" /anthropic betas add redact-thinking-2026-02-12",
|
|
49
|
+
" /anthropic betas add 1m (shortcut for context-1m-2025-08-07)",
|
|
50
|
+
"",
|
|
51
|
+
"Remove: /anthropic betas remove <beta>",
|
|
52
|
+
];
|
|
53
|
+
await sendCommandMessage(sessionID, lines.join("\n"));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (action === "add") {
|
|
58
|
+
const betaInput = args[2]?.trim();
|
|
59
|
+
if (!betaInput) {
|
|
60
|
+
await sendCommandMessage(sessionID, "▣ Anthropic Betas\n\nUsage: /anthropic betas add <beta-name>");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const beta = resolveBetaShortcut(betaInput);
|
|
64
|
+
const fresh = loadConfigFresh();
|
|
65
|
+
const current = fresh.custom_betas || [];
|
|
66
|
+
if (current.includes(beta)) {
|
|
67
|
+
await sendCommandMessage(sessionID, `▣ Anthropic Betas\n\n"${beta}" already added.`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
saveConfig({ custom_betas: [...current, beta] });
|
|
71
|
+
Object.assign(config, loadConfigFresh());
|
|
72
|
+
const fromShortcut = beta !== betaInput;
|
|
73
|
+
await sendCommandMessage(
|
|
74
|
+
sessionID,
|
|
75
|
+
`▣ Anthropic Betas\n\nAdded: ${beta}${fromShortcut ? ` (from shortcut: ${betaInput})` : ""}`,
|
|
76
|
+
);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (action === "remove" || action === "rm") {
|
|
81
|
+
const betaInput = args[2]?.trim();
|
|
82
|
+
if (!betaInput) {
|
|
83
|
+
await sendCommandMessage(sessionID, "▣ Anthropic Betas\n\nUsage: /anthropic betas remove <beta-name>");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const beta = resolveBetaShortcut(betaInput);
|
|
87
|
+
const fresh = loadConfigFresh();
|
|
88
|
+
const current = fresh.custom_betas || [];
|
|
89
|
+
if (!current.includes(beta)) {
|
|
90
|
+
await sendCommandMessage(sessionID, `▣ Anthropic Betas\n\n"${beta}" not in custom betas.`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
saveConfig({ custom_betas: current.filter((b) => b !== beta) });
|
|
94
|
+
Object.assign(config, loadConfigFresh());
|
|
95
|
+
await sendCommandMessage(sessionID, `▣ Anthropic Betas\n\nRemoved: ${beta}`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await sendCommandMessage(sessionID, "▣ Anthropic Betas\n\nUsage: /anthropic betas [add|remove <beta>]");
|
|
100
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Config slash-command handlers (/anthropic config, /anthropic set)
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import type { AnthropicAuthConfig } from "../../config.js";
|
|
6
|
+
import { loadConfigFresh, saveConfig } from "../../config.js";
|
|
7
|
+
|
|
8
|
+
export interface ConfigHandlerDeps {
|
|
9
|
+
sendCommandMessage: (sessionID: string, message: string) => Promise<void>;
|
|
10
|
+
config: AnthropicAuthConfig;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Handle /anthropic config — display current configuration.
|
|
15
|
+
*/
|
|
16
|
+
export async function handleConfigCommand(sessionID: string, deps: ConfigHandlerDeps): Promise<void> {
|
|
17
|
+
const { sendCommandMessage } = deps;
|
|
18
|
+
const fresh = loadConfigFresh();
|
|
19
|
+
const lines = [
|
|
20
|
+
"▣ Anthropic Config",
|
|
21
|
+
"",
|
|
22
|
+
`strategy: ${fresh.account_selection_strategy}`,
|
|
23
|
+
`emulation: ${fresh.signature_emulation.enabled ? "on" : "off"}`,
|
|
24
|
+
`compaction: ${fresh.signature_emulation.prompt_compaction}`,
|
|
25
|
+
`1m-context: ${fresh.override_model_limits.enabled ? "on" : "off"}`,
|
|
26
|
+
`idle-refresh: ${fresh.idle_refresh.enabled ? "on" : "off"}`,
|
|
27
|
+
`debug: ${fresh.debug ? "on" : "off"}`,
|
|
28
|
+
`quiet: ${fresh.toasts.quiet ? "on" : "off"}`,
|
|
29
|
+
`custom_betas: ${fresh.custom_betas.length ? fresh.custom_betas.join(", ") : "(none)"}`,
|
|
30
|
+
];
|
|
31
|
+
await sendCommandMessage(sessionID, lines.join("\n"));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Handle /anthropic set <key> <value> — update a config setting.
|
|
36
|
+
*/
|
|
37
|
+
export async function handleSetCommand(sessionID: string, args: string[], deps: ConfigHandlerDeps): Promise<void> {
|
|
38
|
+
const { sendCommandMessage, config } = deps;
|
|
39
|
+
const key = (args[1] || "").toLowerCase();
|
|
40
|
+
const value = (args[2] || "").toLowerCase();
|
|
41
|
+
const setters: Record<string, () => void> = {
|
|
42
|
+
emulation: () =>
|
|
43
|
+
saveConfig({
|
|
44
|
+
signature_emulation: {
|
|
45
|
+
enabled: value === "on" || value === "1" || value === "true",
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
compaction: () =>
|
|
49
|
+
saveConfig({
|
|
50
|
+
signature_emulation: {
|
|
51
|
+
prompt_compaction: value === "off" ? "off" : "minimal",
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
"1m-context": () =>
|
|
55
|
+
saveConfig({
|
|
56
|
+
override_model_limits: {
|
|
57
|
+
enabled: value === "on" || value === "1" || value === "true",
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
"idle-refresh": () =>
|
|
61
|
+
saveConfig({
|
|
62
|
+
idle_refresh: {
|
|
63
|
+
enabled: value === "on" || value === "1" || value === "true",
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
debug: () =>
|
|
67
|
+
saveConfig({
|
|
68
|
+
debug: value === "on" || value === "1" || value === "true",
|
|
69
|
+
}),
|
|
70
|
+
quiet: () =>
|
|
71
|
+
saveConfig({
|
|
72
|
+
toasts: {
|
|
73
|
+
quiet: value === "on" || value === "1" || value === "true",
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
strategy: () => {
|
|
77
|
+
const valid = ["sticky", "round-robin", "hybrid"];
|
|
78
|
+
if (valid.includes(value))
|
|
79
|
+
saveConfig({ account_selection_strategy: value as "sticky" | "round-robin" | "hybrid" });
|
|
80
|
+
else throw new Error(`Invalid strategy. Valid: ${valid.join(", ")}`);
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (!key || !setters[key]) {
|
|
85
|
+
const keys = Object.keys(setters).join(", ");
|
|
86
|
+
await sendCommandMessage(
|
|
87
|
+
sessionID,
|
|
88
|
+
`▣ Anthropic Set\n\nUsage: /anthropic set <key> <value>\nKeys: ${keys}\nValues: on/off (or specific values for strategy/compaction)`,
|
|
89
|
+
);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!value) {
|
|
93
|
+
await sendCommandMessage(sessionID, `▣ Anthropic Set\n\nMissing value for "${key}".`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
setters[key]();
|
|
97
|
+
Object.assign(config, loadConfigFresh());
|
|
98
|
+
await sendCommandMessage(sessionID, `▣ Anthropic Set\n\n${key} = ${value}`);
|
|
99
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Files API slash-command handler (/anthropic files)
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { basename, resolve } from "node:path";
|
|
7
|
+
import type { AccountManager } from "../../accounts.js";
|
|
8
|
+
import type { ManagedAccount } from "../../token-refresh.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Maximum number of file-to-account pinning entries retained in memory.
|
|
12
|
+
* Bounded to prevent unbounded growth across long sessions that touch many
|
|
13
|
+
* Files API uploads. Eviction is FIFO: when the cap is hit, the oldest entry
|
|
14
|
+
* (Maps preserve insertion order) is dropped before inserting the new one.
|
|
15
|
+
*/
|
|
16
|
+
export const FILE_ACCOUNT_MAP_MAX_SIZE = 1000;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Insert a fileId→accountIndex binding with FIFO eviction when the cap is reached.
|
|
20
|
+
* See {@link FILE_ACCOUNT_MAP_MAX_SIZE} for the rationale.
|
|
21
|
+
*/
|
|
22
|
+
export function capFileAccountMap(fileAccountMap: Map<string, number>, fileId: string, accountIndex: number): void {
|
|
23
|
+
if (fileAccountMap.size >= FILE_ACCOUNT_MAP_MAX_SIZE) {
|
|
24
|
+
const oldestKey = fileAccountMap.keys().next().value;
|
|
25
|
+
if (oldestKey !== undefined) fileAccountMap.delete(oldestKey);
|
|
26
|
+
}
|
|
27
|
+
fileAccountMap.set(fileId, accountIndex);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FilesHandlerDeps {
|
|
31
|
+
sendCommandMessage: (sessionID: string, message: string) => Promise<void>;
|
|
32
|
+
accountManager: AccountManager | null;
|
|
33
|
+
fileAccountMap: Map<string, number>;
|
|
34
|
+
refreshAccountTokenSingleFlight: (account: ManagedAccount) => Promise<string>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type ResolvedAccount = { account: ManagedAccount; label: string };
|
|
38
|
+
|
|
39
|
+
function resolveTargetAccount(accountManager: AccountManager, identifier: string | null): ResolvedAccount | null {
|
|
40
|
+
const accounts = accountManager.getEnabledAccounts();
|
|
41
|
+
if (identifier) {
|
|
42
|
+
const byEmail = accounts.find((a) => a.email === identifier);
|
|
43
|
+
if (byEmail) return { account: byEmail, label: byEmail.email || `Account ${byEmail.index + 1}` };
|
|
44
|
+
const idx = parseInt(identifier, 10);
|
|
45
|
+
if (!isNaN(idx) && idx >= 1) {
|
|
46
|
+
const byIdx = accounts.find((a) => a.index === idx - 1);
|
|
47
|
+
if (byIdx) return { account: byIdx, label: byIdx.email || `Account ${byIdx.index + 1}` };
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const current = accountManager.getCurrentAccount();
|
|
52
|
+
if (!current) return null;
|
|
53
|
+
return { account: current, label: current.email || `Account ${current.index + 1}` };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function getFilesAuth(
|
|
57
|
+
acct: ManagedAccount,
|
|
58
|
+
refreshAccountTokenSingleFlight: (account: ManagedAccount) => Promise<string>,
|
|
59
|
+
) {
|
|
60
|
+
let tok = acct.access;
|
|
61
|
+
if (!tok || !acct.expires || acct.expires < Date.now()) {
|
|
62
|
+
tok = await refreshAccountTokenSingleFlight(acct);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
authorization: `Bearer ${tok}`,
|
|
66
|
+
"anthropic-beta": "oauth-2025-04-20,files-api-2025-04-14",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const API_BASE = "https://api.anthropic.com";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Handle /anthropic files [list|upload|get|delete|download].
|
|
74
|
+
*/
|
|
75
|
+
export async function handleFilesCommand(sessionID: string, args: string[], deps: FilesHandlerDeps): Promise<void> {
|
|
76
|
+
const { sendCommandMessage, accountManager, fileAccountMap, refreshAccountTokenSingleFlight } = deps;
|
|
77
|
+
|
|
78
|
+
let targetAccountId: string | null = null;
|
|
79
|
+
const filteredArgs: string[] = [];
|
|
80
|
+
for (let i = 0; i < args.length; i++) {
|
|
81
|
+
if (args[i] === "--account" && i + 1 < args.length) {
|
|
82
|
+
targetAccountId = args[i + 1];
|
|
83
|
+
i++;
|
|
84
|
+
} else {
|
|
85
|
+
filteredArgs.push(args[i]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const action = (filteredArgs[1] || "").toLowerCase();
|
|
89
|
+
|
|
90
|
+
if (!accountManager || accountManager.getAccountCount() === 0) {
|
|
91
|
+
await sendCommandMessage(
|
|
92
|
+
sessionID,
|
|
93
|
+
"▣ Anthropic Files (error)\n\nNo accounts configured. Use /anthropic login first.",
|
|
94
|
+
);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
if (!action || action === "list") {
|
|
100
|
+
if (targetAccountId) {
|
|
101
|
+
const resolved = resolveTargetAccount(accountManager, targetAccountId);
|
|
102
|
+
if (!resolved) {
|
|
103
|
+
await sendCommandMessage(
|
|
104
|
+
sessionID,
|
|
105
|
+
`▣ Anthropic Files (error)\n\nAccount not found: ${targetAccountId}`,
|
|
106
|
+
);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const { account, label } = resolved;
|
|
110
|
+
const headers = await getFilesAuth(account, refreshAccountTokenSingleFlight);
|
|
111
|
+
const res = await fetch(`${API_BASE}/v1/files`, { headers });
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
const errBody = await res.text();
|
|
114
|
+
await sendCommandMessage(
|
|
115
|
+
sessionID,
|
|
116
|
+
`▣ Anthropic Files (error) [${label}]\n\nHTTP ${res.status}: ${errBody}`,
|
|
117
|
+
);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const data = (await res.json()) as {
|
|
121
|
+
data?: Array<{ id: string; filename: string; size: number; purpose: string }>;
|
|
122
|
+
};
|
|
123
|
+
const files = data.data || [];
|
|
124
|
+
for (const f of files) capFileAccountMap(fileAccountMap, f.id, account.index);
|
|
125
|
+
if (files.length === 0) {
|
|
126
|
+
await sendCommandMessage(sessionID, `▣ Anthropic Files [${label}]\n\nNo files uploaded.`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const lines = [`▣ Anthropic Files [${label}]`, "", `${files.length} file(s):`, ""];
|
|
130
|
+
for (const f of files) {
|
|
131
|
+
const sizeKB = (f.size / 1024).toFixed(1);
|
|
132
|
+
lines.push(` ${f.id} ${f.filename} (${sizeKB} KB, ${f.purpose})`);
|
|
133
|
+
}
|
|
134
|
+
await sendCommandMessage(sessionID, lines.join("\n"));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const accounts = accountManager.getEnabledAccounts();
|
|
139
|
+
const allLines = ["▣ Anthropic Files (all accounts)", ""];
|
|
140
|
+
let totalFiles = 0;
|
|
141
|
+
for (const acct of accounts) {
|
|
142
|
+
const label = acct.email || `Account ${acct.index + 1}`;
|
|
143
|
+
try {
|
|
144
|
+
const headers = await getFilesAuth(acct, refreshAccountTokenSingleFlight);
|
|
145
|
+
const res = await fetch(`${API_BASE}/v1/files`, { headers });
|
|
146
|
+
if (!res.ok) {
|
|
147
|
+
allLines.push(`[${label}] Error: HTTP ${res.status}`);
|
|
148
|
+
allLines.push("");
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const data = (await res.json()) as {
|
|
152
|
+
data?: Array<{ id: string; filename: string; size: number; purpose: string }>;
|
|
153
|
+
};
|
|
154
|
+
const files = data.data || [];
|
|
155
|
+
for (const f of files) capFileAccountMap(fileAccountMap, f.id, acct.index);
|
|
156
|
+
totalFiles += files.length;
|
|
157
|
+
if (files.length === 0) {
|
|
158
|
+
allLines.push(`[${label}] No files`);
|
|
159
|
+
} else {
|
|
160
|
+
allLines.push(`[${label}] ${files.length} file(s):`);
|
|
161
|
+
for (const f of files) {
|
|
162
|
+
const sizeKB = (f.size / 1024).toFixed(1);
|
|
163
|
+
allLines.push(` ${f.id} ${f.filename} (${sizeKB} KB, ${f.purpose})`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
allLines.push("");
|
|
167
|
+
} catch (err) {
|
|
168
|
+
allLines.push(`[${label}] Error: ${(err as Error).message}`);
|
|
169
|
+
allLines.push("");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (totalFiles === 0 && accounts.length > 0) {
|
|
173
|
+
allLines.push(`Total: No files across ${accounts.length} account(s).`);
|
|
174
|
+
} else {
|
|
175
|
+
allLines.push(`Total: ${totalFiles} file(s) across ${accounts.length} account(s).`);
|
|
176
|
+
}
|
|
177
|
+
if (accounts.length > 1) {
|
|
178
|
+
allLines.push("", "Tip: Use --account <email> to target a specific account.");
|
|
179
|
+
}
|
|
180
|
+
await sendCommandMessage(sessionID, allLines.join("\n"));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const resolved = resolveTargetAccount(accountManager, targetAccountId);
|
|
185
|
+
if (!resolved) {
|
|
186
|
+
const errMsg = targetAccountId ? `Account not found: ${targetAccountId}` : "No accounts available.";
|
|
187
|
+
await sendCommandMessage(sessionID, `▣ Anthropic Files (error)\n\n${errMsg}`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const { account, label } = resolved;
|
|
191
|
+
const authHeaders = await getFilesAuth(account, refreshAccountTokenSingleFlight);
|
|
192
|
+
|
|
193
|
+
if (action === "upload") {
|
|
194
|
+
const filePath = filteredArgs.slice(2).join(" ").trim();
|
|
195
|
+
if (!filePath) {
|
|
196
|
+
await sendCommandMessage(
|
|
197
|
+
sessionID,
|
|
198
|
+
"▣ Anthropic Files\n\nUsage: /anthropic files upload <path> [--account <email>]",
|
|
199
|
+
);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const resolvedPath = resolve(filePath);
|
|
203
|
+
if (!existsSync(resolvedPath)) {
|
|
204
|
+
await sendCommandMessage(sessionID, `▣ Anthropic Files (error)\n\nFile not found: ${resolvedPath}`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const content = readFileSync(resolvedPath);
|
|
208
|
+
const filename = basename(resolvedPath);
|
|
209
|
+
const blob = new Blob([content]);
|
|
210
|
+
const form = new FormData();
|
|
211
|
+
form.append("file", blob, filename);
|
|
212
|
+
form.append("purpose", "assistants");
|
|
213
|
+
const res = await fetch(`${API_BASE}/v1/files`, {
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: {
|
|
216
|
+
authorization: authHeaders.authorization,
|
|
217
|
+
"anthropic-beta": "oauth-2025-04-20,files-api-2025-04-14",
|
|
218
|
+
},
|
|
219
|
+
body: form,
|
|
220
|
+
});
|
|
221
|
+
if (!res.ok) {
|
|
222
|
+
const errBody = await res.text();
|
|
223
|
+
await sendCommandMessage(
|
|
224
|
+
sessionID,
|
|
225
|
+
`▣ Anthropic Files (error) [${label}]\n\nUpload failed (HTTP ${res.status}): ${errBody}`,
|
|
226
|
+
);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const file = (await res.json()) as { id: string; filename: string; size?: number };
|
|
230
|
+
const sizeKB = ((file.size || 0) / 1024).toFixed(1);
|
|
231
|
+
capFileAccountMap(fileAccountMap, file.id, account.index);
|
|
232
|
+
await sendCommandMessage(
|
|
233
|
+
sessionID,
|
|
234
|
+
`▣ Anthropic Files [${label}]\n\nUploaded: ${file.id}\n Filename: ${file.filename}\n Size: ${sizeKB} KB`,
|
|
235
|
+
);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (action === "get" || action === "info") {
|
|
240
|
+
const fileId = filteredArgs[2]?.trim();
|
|
241
|
+
if (!fileId) {
|
|
242
|
+
await sendCommandMessage(
|
|
243
|
+
sessionID,
|
|
244
|
+
"▣ Anthropic Files\n\nUsage: /anthropic files get <file_id> [--account <email>]",
|
|
245
|
+
);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const res = await fetch(`${API_BASE}/v1/files/${encodeURIComponent(fileId)}`, { headers: authHeaders });
|
|
249
|
+
if (!res.ok) {
|
|
250
|
+
const errBody = await res.text();
|
|
251
|
+
await sendCommandMessage(
|
|
252
|
+
sessionID,
|
|
253
|
+
`▣ Anthropic Files (error) [${label}]\n\nHTTP ${res.status}: ${errBody}`,
|
|
254
|
+
);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const file = (await res.json()) as {
|
|
258
|
+
id: string;
|
|
259
|
+
filename: string;
|
|
260
|
+
purpose: string;
|
|
261
|
+
size?: number;
|
|
262
|
+
mime_type?: string;
|
|
263
|
+
created_at?: string;
|
|
264
|
+
};
|
|
265
|
+
capFileAccountMap(fileAccountMap, file.id, account.index);
|
|
266
|
+
const lines = [
|
|
267
|
+
`▣ Anthropic Files [${label}]`,
|
|
268
|
+
"",
|
|
269
|
+
` ID: ${file.id}`,
|
|
270
|
+
` Filename: ${file.filename}`,
|
|
271
|
+
` Purpose: ${file.purpose}`,
|
|
272
|
+
` Size: ${((file.size || 0) / 1024).toFixed(1)} KB`,
|
|
273
|
+
` Type: ${file.mime_type || "unknown"}`,
|
|
274
|
+
` Created: ${file.created_at || "unknown"}`,
|
|
275
|
+
];
|
|
276
|
+
await sendCommandMessage(sessionID, lines.join("\n"));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (action === "delete" || action === "rm") {
|
|
281
|
+
const fileId = filteredArgs[2]?.trim();
|
|
282
|
+
if (!fileId) {
|
|
283
|
+
await sendCommandMessage(
|
|
284
|
+
sessionID,
|
|
285
|
+
"▣ Anthropic Files\n\nUsage: /anthropic files delete <file_id> [--account <email>]",
|
|
286
|
+
);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const res = await fetch(`${API_BASE}/v1/files/${encodeURIComponent(fileId)}`, {
|
|
290
|
+
method: "DELETE",
|
|
291
|
+
headers: authHeaders,
|
|
292
|
+
});
|
|
293
|
+
if (!res.ok) {
|
|
294
|
+
const errBody = await res.text();
|
|
295
|
+
await sendCommandMessage(
|
|
296
|
+
sessionID,
|
|
297
|
+
`▣ Anthropic Files (error) [${label}]\n\nHTTP ${res.status}: ${errBody}`,
|
|
298
|
+
);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
fileAccountMap.delete(fileId);
|
|
302
|
+
await sendCommandMessage(sessionID, `▣ Anthropic Files [${label}]\n\nDeleted: ${fileId}`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (action === "download" || action === "dl") {
|
|
307
|
+
const fileId = filteredArgs[2]?.trim();
|
|
308
|
+
if (!fileId) {
|
|
309
|
+
await sendCommandMessage(
|
|
310
|
+
sessionID,
|
|
311
|
+
"▣ Anthropic Files\n\nUsage: /anthropic files download <file_id> [output_path] [--account <email>]",
|
|
312
|
+
);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const outputPath = filteredArgs.slice(3).join(" ").trim();
|
|
316
|
+
const metaRes = await fetch(`${API_BASE}/v1/files/${encodeURIComponent(fileId)}`, { headers: authHeaders });
|
|
317
|
+
if (!metaRes.ok) {
|
|
318
|
+
const errBody = await metaRes.text();
|
|
319
|
+
await sendCommandMessage(
|
|
320
|
+
sessionID,
|
|
321
|
+
`▣ Anthropic Files (error) [${label}]\n\nHTTP ${metaRes.status}: ${errBody}`,
|
|
322
|
+
);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const meta = (await metaRes.json()) as { filename: string };
|
|
326
|
+
const savePath = outputPath ? resolve(outputPath) : resolve(meta.filename);
|
|
327
|
+
const res = await fetch(`${API_BASE}/v1/files/${encodeURIComponent(fileId)}/content`, {
|
|
328
|
+
headers: authHeaders,
|
|
329
|
+
});
|
|
330
|
+
if (!res.ok) {
|
|
331
|
+
const errBody = await res.text();
|
|
332
|
+
await sendCommandMessage(
|
|
333
|
+
sessionID,
|
|
334
|
+
`▣ Anthropic Files (error) [${label}]\n\nDownload failed (HTTP ${res.status}): ${errBody}`,
|
|
335
|
+
);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
339
|
+
writeFileSync(savePath, buffer);
|
|
340
|
+
const sizeKB = (buffer.length / 1024).toFixed(1);
|
|
341
|
+
await sendCommandMessage(
|
|
342
|
+
sessionID,
|
|
343
|
+
`▣ Anthropic Files [${label}]\n\nDownloaded: ${meta.filename}\n Saved to: ${savePath}\n Size: ${sizeKB} KB`,
|
|
344
|
+
);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const helpLines = [
|
|
349
|
+
"▣ Anthropic Files",
|
|
350
|
+
"",
|
|
351
|
+
"Usage: /anthropic files <action> [--account <email|index>]",
|
|
352
|
+
"",
|
|
353
|
+
"Actions:",
|
|
354
|
+
" list List uploaded files (all accounts if no --account)",
|
|
355
|
+
" upload <path> Upload a file (max 350MB)",
|
|
356
|
+
" get <file_id> Get file metadata",
|
|
357
|
+
" delete <file_id> Delete a file",
|
|
358
|
+
" download <file_id> [path] Download file content",
|
|
359
|
+
"",
|
|
360
|
+
"Options:",
|
|
361
|
+
" --account <email|index> Target a specific account (1-based index)",
|
|
362
|
+
"",
|
|
363
|
+
"Supported formats: PDF, DOCX, TXT, CSV, Excel, Markdown, images",
|
|
364
|
+
"Files can be referenced by file_id in Messages API requests.",
|
|
365
|
+
"",
|
|
366
|
+
"When using round-robin, file_ids are automatically pinned to the",
|
|
367
|
+
"account that owns them for Messages API requests.",
|
|
368
|
+
];
|
|
369
|
+
await sendCommandMessage(sessionID, helpLines.join("\n"));
|
|
370
|
+
return;
|
|
371
|
+
} catch (err) {
|
|
372
|
+
await sendCommandMessage(sessionID, `▣ Anthropic Files (error)\n\n${(err as Error).message}`);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
}
|