@coda-console/claude 0.0.0 → 1.4.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.
@@ -0,0 +1,231 @@
1
+ // What Claude Code keeps going beside a turn, told to Coda as waits of its own (docs/agent-protocol.md「_coda/wait」): its
2
+ // background tasks and subagents as the SDK reports them, and the wakeups it scheduled as its Stop hook lists them. A
3
+ // subagent's session is the tool call that launched it; continued (SendMessage), it is another run under the same session.
4
+ // What a subagent launches (`parent_task_id`) is told in that subagent's session, once a wait has told Coda that session.
5
+ // A task that ends while no turn goes will wake Claude for its result: until a turn takes it up, or the session says idle,
6
+ // it is still waited on. A turn Claude begins on its own handles what woke it (`takeUp`): what its opening message names,
7
+ // else the tasks that ended, else the one wakeup due, else a wait of the turn's own; what it handles ends with that turn.
8
+ const FINAL = { completed: 'done', failed: 'failed', killed: 'cancelled', stopped: 'cancelled', ended: 'done' };
9
+ const open = state => state === 'waiting' || state === 'handling';
10
+ const cut = (value, limit) => typeof value === 'string' && value ? value.slice(0, limit) : undefined;
11
+
12
+ export class ClaudeWaits {
13
+ // `say`: a wait's report, and its session when that is a subagent's; `going`: whether a turn goes; `registered`: a subagent's
14
+ // session is known.
15
+ constructor({ say, going = () => false, registered = () => {}, now = Date.now }) {
16
+ this.say = say; this.going = going; this.registered = registered; this.now = now;
17
+ this.tasks = new Map(); this.crons = new Map(); this.own = new Set(); this.turns = 0;
18
+ // Commands in the foreground, by task id: each is its own tool call, which the turn (or the subagent) blocks on.
19
+ this.foreground = new Map();
20
+ // What is known of a launching tool call before its task is: a subagent's model, a command's output file.
21
+ this.models = new Map(); this.outputs = new Map();
22
+ }
23
+ get handling() { return this.own.size > 0 || [...this.tasks.values(), ...this.crons.values()].some(w => w.state === 'handling'); }
24
+ // Whether anything is still waited on: what ends with the Claude process that runs it.
25
+ get pending() { return this.own.size > 0 || [...this.tasks.values(), ...this.crons.values()].some(w => open(w.state)); }
26
+ report(wait, state, fields = {}) {
27
+ wait.state = state;
28
+ const params = { waitId: wait.waitId, state, ...Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined)) };
29
+ // Kept while the session it is in is not yet told (`named`).
30
+ if (wait.held) wait.held.push(params); else this.say(params, wait.sessionId);
31
+ }
32
+ // A task begun (`task_started`, `start`), or listed among those in the background (`listed`).
33
+ started(native, { start = false, listed = false } = {}) {
34
+ if (native.ambient || typeof native.task_id !== 'string' || !native.task_id) return;
35
+ // A command in the foreground is not waited on until it is put in the background (`backgrounded`). A subagent in the
36
+ // foreground still is: its session is that card's.
37
+ if (native.is_backgrounded === false && native.task_type !== 'local_agent' && !this.tasks.has(native.task_id)) {
38
+ this.foreground.set(native.task_id, native);
39
+ return;
40
+ }
41
+ const known = this.tasks.get(native.task_id);
42
+ if (known) {
43
+ if (listed) known.listed = true;
44
+ // A snapshot names no tool call and may come before its run's start, even after its end: that start says the launching
45
+ // call, and with it a subagent's session. A start of the run already known is that run's, never another.
46
+ const sameRun = native.run_id && known.runId ? native.run_id === known.runId : !known.toolUseId;
47
+ if (start && sameRun && !known.toolUseId && native.tool_use_id) {
48
+ const toolUseId = cut(native.tool_use_id, 200), subagent = known.kind === 'subagent';
49
+ Object.assign(known, { toolUseId, runId: known.runId ?? native.run_id }, subagent ? { childSessionId: toolUseId } : {});
50
+ if (open(known.state)) this.report(known, known.state, { childSessionId: subagent ? toolUseId : undefined, toolCallId: toolUseId,
51
+ outputFile: subagent ? undefined : this.outputs.get(toolUseId), model: subagent ? this.models.get(toolUseId) : undefined });
52
+ if (subagent && !known.held) this.named(known);
53
+ // An end held for its session is told now while a turn goes, which takes it in.
54
+ if (known.final !== undefined && known.state === 'waiting' && this.going()) { this.report(known, known.final); known.final = undefined; }
55
+ return;
56
+ }
57
+ // Only a start of another run under another call brings a subagent that ended back; snapshots and repeats cannot.
58
+ if (!start || sameRun || known.kind !== 'subagent' || (open(known.state) && known.final === undefined) || !native.tool_use_id || native.tool_use_id === known.toolUseId) return;
59
+ // Its last run ends before the next begins; one the turn going handles leaves it to a wait of the turn's own.
60
+ if (known.state === 'handling' && this.own.size === 0) this.hold();
61
+ if (known.final !== undefined) { this.report(known, known.final); known.final = undefined; }
62
+ known.run++; Object.assign(known, { waitId: `${known.taskId}#${known.run}`, toolUseId: native.tool_use_id, runId: native.run_id });
63
+ this.report(known, 'waiting', { kind: 'subagent', title: cut(native.description, 500) ?? '子 Agent', childSessionId: known.childSessionId,
64
+ stoppable: true, model: this.models.get(known.childSessionId) });
65
+ return;
66
+ }
67
+ const subagent = native.task_type === 'local_agent', toolUseId = cut(native.tool_use_id, 200);
68
+ const task = { taskId: native.task_id, waitId: native.task_id.slice(0, 200), run: 1, kind: subagent ? 'subagent' : 'task', toolUseId, runId: native.run_id,
69
+ listed: listed || native.is_backgrounded === true, parentTaskId: native.parent_task_id, ...(subagent ? { childSessionId: toolUseId ?? native.task_id.slice(0, 200) } : {}) };
70
+ if (task.parentTaskId) {
71
+ const parent = this.tasks.get(task.parentTaskId);
72
+ if (parent?.toolUseId && !parent.held) task.sessionId = parent.childSessionId; else task.held = [];
73
+ }
74
+ this.tasks.set(task.taskId, task);
75
+ this.report(task, 'waiting', { kind: task.kind, title: cut(native.description, 500) ?? (subagent ? '子 Agent' : '后台任务'), stoppable: true,
76
+ childSessionId: task.childSessionId, toolCallId: toolUseId, outputFile: this.outputs.get(toolUseId),
77
+ model: subagent ? this.models.get(task.childSessionId) : undefined });
78
+ if (subagent && !task.held) this.named(task);
79
+ }
80
+ // A subagent's session told to Coda: what it said there comes after, and what it launched is told there, held reports first.
81
+ named(subagent) {
82
+ this.registered(subagent.childSessionId);
83
+ if (!subagent.toolUseId) return;
84
+ for (const task of this.tasks.values()) {
85
+ if (!task.held || task.parentTaskId !== subagent.taskId) continue;
86
+ const held = task.held;
87
+ task.held = undefined; task.sessionId = subagent.childSessionId;
88
+ for (const params of held) this.say(params, task.sessionId);
89
+ if (task.kind === 'subagent') this.named(task);
90
+ }
91
+ }
92
+ // A command in the foreground put in the background: from now on it is waited on, as begun there.
93
+ backgrounded(taskId) {
94
+ const native = this.foreground.get(taskId);
95
+ if (!native) return;
96
+ this.foreground.delete(taskId);
97
+ this.started({ ...native, is_backgrounded: true }, { start: true });
98
+ }
99
+ renamed(taskId, description) {
100
+ const task = this.tasks.get(taskId);
101
+ if (task && open(task.state) && cut(description, 500)) this.report(task, task.state, { title: cut(description, 500) });
102
+ }
103
+ // A task's end (`status` as the SDK says it; `ended` when it left the list without one): told now while a turn goes, which
104
+ // takes in its result; otherwise once a turn has taken it up.
105
+ ended(taskId, status, { outputFile } = {}) {
106
+ this.foreground.delete(taskId);
107
+ const task = this.tasks.get(taskId), final = FINAL[status];
108
+ if (!task || !final || !open(task.state)) return;
109
+ if (task.final !== undefined) { if (status !== 'ended') task.final = final; return; }
110
+ const fields = { outputFile: cut(outputFile, 4096) };
111
+ // A subagent whose session is not yet known ends once it is (`started`), or as the turn or the session settles.
112
+ if (task.state === 'handling' || !this.going() || (task.kind === 'subagent' && !task.toolUseId)) {
113
+ Object.assign(task, { final, endedAt: this.now() });
114
+ if (fields.outputFile) this.report(task, task.state, fields);
115
+ } else this.report(task, final, fields);
116
+ }
117
+ snapshot(list) {
118
+ const live = new Set(list.map(native => native.task_id));
119
+ for (const task of this.tasks.values()) if (task.listed && !live.has(task.taskId)) this.ended(task.taskId, 'ended');
120
+ for (const native of list) { this.backgrounded(native.task_id); this.started(native, { listed: true }); }
121
+ }
122
+ // The Stop hook's list of the wakeups the session has, complete each time.
123
+ scheduled(list) {
124
+ const listed = new Set();
125
+ for (const native of list) {
126
+ listed.add(native.id);
127
+ if (this.crons.has(native.id)) continue;
128
+ const cron = { waitId: native.id.slice(0, 200), schedule: native.schedule, recurring: native.recurring, prompt: native.prompt, at: nextRun(native.schedule, this.now()) };
129
+ this.crons.set(native.id, cron);
130
+ this.report(cron, 'waiting', { kind: 'timer', title: cut(native.prompt, 100) ?? cut(native.schedule, 500), detail: cut(native.schedule, 4000),
131
+ recurring: native.recurring, stoppable: false, at: cron.at === null ? undefined : new Date(cron.at).toISOString() });
132
+ }
133
+ for (const [id, cron] of this.crons) {
134
+ if (listed.has(id) || !open(cron.state)) continue;
135
+ if (cron.state === 'handling') cron.final = 'done'; else this.report(cron, 'done');
136
+ }
137
+ }
138
+ // The tasks that ended since the last turn: taken in by the turn that ended now, or not to wake Claude now it is idle.
139
+ settled() {
140
+ for (const task of this.tasks.values()) if (task.final !== undefined && task.state === 'waiting') { this.report(task, task.final); task.final = undefined; }
141
+ }
142
+ // A turn Claude began on its own: what it handles. `origin`, `text`: its opening message's, when that came first.
143
+ takeUp({ origin, text } = {}) {
144
+ const pending = [...this.tasks.values()].filter(task => task.final !== undefined && task.state === 'waiting');
145
+ const crons = [...this.crons.values()].filter(cron => open(cron.state));
146
+ let handled = [];
147
+ if (origin?.kind === 'task-notification') handled = origin.subkind === 'scheduled-trigger' ? crons.filter(cron => cron.prompt && cron.prompt === text)
148
+ : [...this.tasks.values()].filter(task => origin.runId && task.runId === origin.runId && open(task.state));
149
+ if (!handled.length) handled = pending.sort((a, b) => a.endedAt - b.endedAt);
150
+ if (!handled.length) {
151
+ // Which came due is not told: the one due, or else the only one there is.
152
+ const due = crons.filter(cron => cron.at !== null && cron.at <= this.now() + 60_000);
153
+ if ((due.length || crons.length) === 1) handled = due.length ? due : crons;
154
+ }
155
+ if (handled.length) for (const wait of handled) this.report(wait, 'handling');
156
+ else this.hold();
157
+ }
158
+ hold() {
159
+ const own = { waitId: `turn-${++this.turns}` };
160
+ this.own.add(own);
161
+ this.report(own, 'handling', { kind: 'task', title: 'Claude 自主继续', stoppable: false });
162
+ }
163
+ // That turn's end: what it handled ended with it, a recurring wakeup waits for its next time.
164
+ finished() {
165
+ for (const task of this.tasks.values()) if (task.state === 'handling') { this.report(task, task.final ?? 'done'); task.final = undefined; }
166
+ for (const cron of this.crons.values()) {
167
+ if (cron.state !== 'handling') continue;
168
+ if (cron.final !== undefined || !cron.recurring) { this.report(cron, 'done'); continue; }
169
+ cron.at = nextRun(cron.schedule, this.now());
170
+ this.report(cron, 'waiting', { at: cron.at === null ? undefined : new Date(cron.at).toISOString() });
171
+ }
172
+ for (const own of this.own) this.report(own, 'done');
173
+ this.own.clear();
174
+ }
175
+ // A stop Coda asks of a wait: the task to stop when it still goes (null: none). One that ended and is yet to be taken in is
176
+ // not waited for any more: it ends now ('').
177
+ stop(waitId) {
178
+ const task = [...this.tasks.values()].find(task => task.waitId === waitId && open(task.state));
179
+ if (!task) return null;
180
+ if (task.final === undefined) return task.taskId;
181
+ if (task.state === 'waiting') { this.report(task, task.final); task.final = undefined; }
182
+ return '';
183
+ }
184
+ childSession(agentId) { return typeof agentId === 'string' ? this.tasks.get(agentId)?.childSessionId ?? null : null; }
185
+ // A subagent's actual model, from its own replies (which can come before its task).
186
+ model(toolUseId, model) {
187
+ if (typeof model !== 'string' || !model.trim() || model === '<synthetic>') return;
188
+ this.models.set(toolUseId, model);
189
+ const task = [...this.tasks.values()].find(task => task.childSessionId === toolUseId && open(task.state));
190
+ if (task && task.model !== model) { task.model = model; this.report(task, task.state, { model: cut(model, 200) }); }
191
+ }
192
+ // Where a command put in the background writes, as its launch said.
193
+ outputFile(toolUseId, path) {
194
+ this.outputs.set(toolUseId, path);
195
+ const task = [...this.tasks.values()].find(task => task.toolUseId === toolUseId && task.kind === 'task' && open(task.state));
196
+ if (task) this.report(task, task.state, { outputFile: cut(path, 4096) });
197
+ }
198
+ }
199
+
200
+ // The next minute after `from` a five-field cron schedule matches, in local time (as Claude Code schedules); null when it
201
+ // cannot be read or never matches within five years.
202
+ export function nextRun(schedule, from = Date.now()) {
203
+ const fields = typeof schedule === 'string' ? schedule.trim().split(/\s+/) : [];
204
+ if (fields.length !== 5) return null;
205
+ const [minutes, hours, days, months, weekdays] = fields.map((field, i) => values(field, ...[[0, 59], [0, 23], [1, 31], [1, 12], [0, 7]][i]));
206
+ if (![minutes, hours, days, months, weekdays].every(Boolean)) return null;
207
+ if (weekdays.has(7)) weekdays.add(0);
208
+ // Both day fields restricted: either matches.
209
+ const day = date => fields[2] === '*' ? fields[4] === '*' || weekdays.has(date.getDay())
210
+ : fields[4] === '*' ? days.has(date.getDate()) : days.has(date.getDate()) || weekdays.has(date.getDay());
211
+ const t = new Date(from); t.setSeconds(0, 0); t.setMinutes(t.getMinutes() + 1);
212
+ for (const limit = from + 5 * 366 * 86_400_000; t.getTime() < limit;) {
213
+ if (!months.has(t.getMonth() + 1)) { t.setMonth(t.getMonth() + 1, 1); t.setHours(0, 0, 0, 0); }
214
+ else if (!day(t)) { t.setDate(t.getDate() + 1); t.setHours(0, 0, 0, 0); }
215
+ else if (!hours.has(t.getHours())) t.setHours(t.getHours() + 1, 0, 0, 0);
216
+ else if (!minutes.has(t.getMinutes())) t.setMinutes(t.getMinutes() + 1, 0, 0);
217
+ else return t.getTime();
218
+ }
219
+ return null;
220
+ }
221
+ function values(field, min, max) {
222
+ const set = new Set();
223
+ for (const part of field.split(',')) {
224
+ const m = /^(?:(\*)|(\d+)(?:-(\d+))?)(?:\/(\d+))?$/.exec(part);
225
+ if (!m) return null;
226
+ const low = m[1] ? min : Number(m[2]), high = m[1] ? max : m[3] !== undefined ? Number(m[3]) : m[4] ? max : low, step = m[4] ? Number(m[4]) : 1;
227
+ if (low < min || high > max || low > high || step < 1) return null;
228
+ for (let v = low; v <= high; v += step) set.add(v);
229
+ }
230
+ return set;
231
+ }
@@ -0,0 +1,15 @@
1
+ # Claude 品牌图形
2
+
3
+ 仅用于标识会话所用的服务,不作为 Coda 自身标志。商标归各自权利人所有。
4
+
5
+ - `claude.svg`:取自 [Claude 官网](https://claude.com/) 导航字标中的独立花形路径,保留官方 `#D97757` 色和几何形状;供 Claude SDK 使用。
6
+
7
+ 提取日期:2026-09-19。SVG 是可审阅的源文件,PDF 是由 CairoSVG 转换的等比矢量资源,清单的 `icon` 指向 PDF。重新生成 PDF(开发工具,不是构建或运行依赖):
8
+
9
+ ```sh
10
+ uv run --with cairosvg python - <<'PY'
11
+ import cairosvg
12
+ for name in ['claude']:
13
+ cairosvg.svg2pdf(url=f'{name}.svg', write_to=f'{name}.pdf')
14
+ PY
15
+ ```
Binary file
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="125" height="125" viewBox="0 0 125 125">
2
+ <path d="M54.375 118.75L56.125 111L58.125 101L59.75 93L61.25 83.125L62.125 79.875L62 79.625L61.375 79.75L53.875 90L42.5 105.375L33.5 114.875L31.375 115.75L27.625 113.875L28 110.375L30.125 107.375L42.5 91.5L50 81.625L54.875 76L54.75 75.25H54.5L21.5 96.75L15.625 97.5L13 95.125L13.375 91.25L14.625 90L24.5 83.125L49.125 69.375L49.5 68.125L49.125 67.5H47.875L43.75 67.25L29.75 66.875L17.625 66.375L5.75 65.75L2.75 65.125L0 61.375L0.25 59.5L2.75 57.875L6.375 58.125L14.25 58.75L26.125 59.5L34.75 60L47.5 61.375H49.5L49.75 60.5L49.125 60L48.625 59.5L36.25 51.25L23 42.5L16 37.375L12.25 34.75L10.375 32.375L9.625 27.125L13 23.375L17.625 23.75L18.75 24L23.375 27.625L33.25 35.25L46.25 44.875L48.125 46.375L49 45.875V45.5L48.125 44.125L41.125 31.375L33.625 18.375L30.25 13L29.375 9.75C29.0417 8.625 28.875 7.375 28.875 6L32.75 0.750006L34.875 0L40.125 0.750006L42.25 2.625L45.5 10L50.625 21.625L58.75 37.375L61.125 42.125L62.375 46.375L62.875 47.75H63.75V47L64.375 38L65.625 27.125L66.875 13.125L67.25 9.125L69.25 4.375L73.125 1.87501L76.125 3.25L78.625 6.875L78.25 9.125L76.875 18.75L73.875 33.875L72 44.125H73.125L74.375 42.75L79.5 36L88.125 25.25L91.875 21L96.375 16.25L99.25 14H104.625L108.5 19.875L106.75 26L101.25 33L96.625 38.875L90 47.75L86 54.875L86.375 55.375H87.25L102.125 52.125L110.25 50.75L119.75 49.125L124.125 51.125L124.625 53.125L122.875 57.375L112.625 59.875L100.625 62.25L82.75 66.5L82.5 66.625L82.75 67L90.75 67.75L94.25 68H102.75L118.5 69.125L122.625 71.875L125 75.125L124.625 77.75L118.25 80.875L109.75 78.875L89.75 74.125L83 72.5H82V73L87.75 78.625L98.125 88L111.25 100.125L111.875 103.125L110.25 105.625L108.5 105.375L97 96.625L92.5 92.75L82.5 84.375H81.875V85.25L84.125 88.625L96.375 107L97 112.625L96.125 114.375L92.875 115.5L89.5 114.875L82.25 104.875L74.875 93.5L68.875 83.375L68.25 83.875L64.625 121.625L63 123.5L59.25 125L56.125 122.625L54.375 118.75Z" fill="#D97757"></path>
3
+ </svg>
package/package.json CHANGED
@@ -1,7 +1,63 @@
1
1
  {
2
2
  "name": "@coda-console/claude",
3
- "version": "0.0.0",
4
- "description": "Reserved for the first Coda plugin release.",
3
+ "id": "claude",
4
+ "displayName": "Claude",
5
+ "description": "Claude SDK Agent:Coda 基于 Claude Agent SDK 写的 ACP 入口,一个进程一个 SDK 会话;资源页的「Claude」面板显示会话报告的额度,可从 Coda 登录。",
6
+ "version": "1.4.0",
7
+ "dependencies": {
8
+ "@agentclientprotocol/sdk": "1.4.0",
9
+ "@anthropic-ai/claude-agent-sdk": "0.3.296"
10
+ },
11
+ "contributes": {
12
+ "agents": [
13
+ {
14
+ "id": "claude-sdk",
15
+ "displayName": "Claude SDK",
16
+ "icon": {
17
+ "light": "icons/claude.pdf"
18
+ },
19
+ "command": "exec \"$CODA_NODE\" \"$CODA_PLUGIN_PATH/agent/entry.mjs\"",
20
+ "modes": {
21
+ "title": "审批模式",
22
+ "unset": "人工审批",
23
+ "auto": "auto",
24
+ "full": "bypassPermissions"
25
+ },
26
+ "skillsDir": ".claude/skills",
27
+ "configFiles": [
28
+ "${CLAUDE_CONFIG_DIR:-~/.claude}/settings.json",
29
+ ".claude/settings.json",
30
+ ".claude/settings.local.json"
31
+ ],
32
+ "setupHint": "请在运行机配置 Claude SDK 认证;若指定了 token 文件,请检查路径和权限。"
33
+ }
34
+ ],
35
+ "configuration": {
36
+ "title": "Claude",
37
+ "properties": {
38
+ "claude.modelProvider": {
39
+ "type": "string",
40
+ "format": "coda-model-provider",
41
+ "title": "模型服务",
42
+ "order": 1,
43
+ "description": "选一个插件提供的模型服务(如 Magpie),它列出的模型和 Claude 自己的模型一起出现在会话的模型里;选中它的模型,那个会话就走这个服务,Claude 自己的模型照旧。只接 Anthropic 接口(anthropic-messages)的服务。"
44
+ }
45
+ }
46
+ },
47
+ "panels": [
48
+ {
49
+ "id": "limits",
50
+ "title": "Claude",
51
+ "command": "node \"$CODA_PLUGIN_PATH/scripts/panel.mjs\"",
52
+ "signIn": {
53
+ "command": "exec node \"$CODA_PLUGIN_PATH/scripts/login.mjs\"",
54
+ "installed": "node \"$CODA_PLUGIN_PATH/scripts/login.mjs\" --installed",
55
+ "input": "authorizationCode",
56
+ "note": "登录保存在这台机器上,供 Claude SDK 使用;额度在会话运行后出现。这台机器若已配置 OAuth token、API key 或云服务商,会话仍优先用那些配置。"
57
+ }
58
+ }
59
+ ]
60
+ },
5
61
  "license": "MIT",
6
62
  "repository": {
7
63
  "type": "git",
@@ -10,5 +66,10 @@
10
66
  },
11
67
  "publishConfig": {
12
68
  "access": "public"
13
- }
69
+ },
70
+ "files": [
71
+ "agent",
72
+ "scripts",
73
+ "icons"
74
+ ]
14
75
  }
@@ -0,0 +1,76 @@
1
+ // The「Claude」panel's login (`signIn`): `claude auth login --claudeai` of the CLI that comes with the plugin's own Claude
2
+ // SDK (the one its sessions run), never another on the PATH. Coda shows the authorization link this prints
3
+ // and writes the code the page gave back to its input, once (docs/plugins.md「面板的登录」). Only the official link is
4
+ // passed on, and only a code bound to it is handed to the CLI, which does the exchange, keeps and renews the credentials.
5
+ // What the CLI says otherwise is not passed on: it may carry the code. `--installed` says whether that CLI is there.
6
+ import { spawn } from 'node:child_process';
7
+ import { createRequire } from 'node:module';
8
+ import { resolve } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { stripVTControlCharacters } from 'node:util';
11
+
12
+ // Resolved beside the SDK, as its query() resolves it.
13
+ export function claudeBinary() {
14
+ const sdk = createRequire(createRequire(import.meta.url).resolve('@anthropic-ai/claude-agent-sdk'));
15
+ const platform = process.platform, arch = process.arch;
16
+ const musl = platform === 'linux' && !process.report.getReport().header.glibcVersionRuntime;
17
+ for (const suffix of platform === 'linux' ? (musl ? ['-musl', ''] : ['', '-musl']) : ['']) {
18
+ try { return sdk.resolve(`@anthropic-ai/claude-agent-sdk-${platform}-${arch}${suffix}/claude${platform === 'win32' ? '.exe' : ''}`); }
19
+ catch {}
20
+ }
21
+ return null;
22
+ }
23
+ // The official authorization link among whole lines of what the CLI printed, terminal hyperlinks included.
24
+ export function authorization(output) {
25
+ const lines = stripVTControlCharacters(output.slice(0, output.lastIndexOf('\n') + 1));
26
+ for (const match of lines.matchAll(/https:\/\/[^\s]+/g)) {
27
+ try {
28
+ const url = new URL(match[0]);
29
+ if (((url.origin === 'https://claude.ai' && url.pathname === '/oauth/authorize') ||
30
+ (url.origin === 'https://claude.com' && url.pathname === '/cai/oauth/authorize')) &&
31
+ url.searchParams.get('state') && url.searchParams.get('code_challenge') && url.searchParams.get('response_type') === 'code') return url;
32
+ } catch {}
33
+ }
34
+ return null;
35
+ }
36
+ // The code the page gives is `<code>#<state>`: one for another login is not handed over.
37
+ export const bound = (code, url) => /^[A-Za-z0-9_-]+#[A-Za-z0-9_-]+$/.test(code) && code.split('#')[1] === url.searchParams.get('state');
38
+
39
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
40
+ const binary = claudeBinary();
41
+ if (process.argv[2] === '--installed') process.exit(binary ? 0 : 1);
42
+ if (!binary) { console.error('找不到 Claude SDK 自带的 CLI'); process.exit(1); }
43
+ const env = { ...process.env, BROWSER: '/usr/bin/true', NO_COLOR: '1' };
44
+ // auth login otherwise silently imports this token instead of opening a fresh browser login.
45
+ delete env.CLAUDE_CODE_OAUTH_REFRESH_TOKEN; delete env.CLAUDE_CODE_OAUTH_SCOPES;
46
+ const child = spawn(binary, ['auth', 'login', '--claudeai'], { env, stdio: ['pipe', 'pipe', 'pipe'] });
47
+ child.stdin.on('error', () => {});
48
+ let output = '', url = null, failed = null;
49
+ const heard = chunk => {
50
+ if (url) return;
51
+ output += chunk;
52
+ url = authorization(output);
53
+ if (url) console.log(url.href);
54
+ };
55
+ child.stdout.on('data', heard); child.stderr.on('data', heard);
56
+ // Coda writes the code once, ending it with a newline.
57
+ let typed = '';
58
+ process.stdin.setEncoding('utf8').on('data', chunk => {
59
+ typed += chunk;
60
+ const end = typed.indexOf('\n');
61
+ if (end < 0) return;
62
+ const code = typed.slice(0, end).trim();
63
+ process.stdin.pause();
64
+ if (url && bound(code, url)) return child.stdin.write(`${code}\n`);
65
+ failed = '授权码不对:要贴这次登录的网页给出的完整授权码,请重新登录。';
66
+ child.kill();
67
+ });
68
+ child.on('error', () => { failed ??= 'Claude CLI 没能启动'; });
69
+ // Coda ends the whole group; one ended alone takes its CLI along.
70
+ for (const signal of ['SIGTERM', 'SIGINT']) process.on(signal, () => { child.kill(); process.exit(1); });
71
+ child.on('close', code => {
72
+ if (code === 0 && !failed) process.exit(0);
73
+ console.error(failed ?? 'Claude 登录没有完成,请重新登录。');
74
+ process.exit(1);
75
+ });
76
+ }
@@ -0,0 +1,6 @@
1
+ // The「Claude」panel's reading, printed once as `{ items }`, from what the sessions wrote into this plugin's state.
2
+ import { join } from 'node:path';
3
+ import { ClaudeSessionLimits } from '../agent/limits.mjs';
4
+ import { panel } from './quota.mjs';
5
+
6
+ console.log(JSON.stringify(panel(new ClaudeSessionLimits({ path: process.env.CODA_PLUGIN_STATE ? join(process.env.CODA_PLUGIN_STATE, 'limits.json') : null }))));
@@ -0,0 +1,11 @@
1
+ // What the「Claude」panel shows (docs/plugins.md「内置插件:Claude」): the plan windows this machine's Claude sessions last
2
+ // reported (agent/limits.mjs). Nothing reported yet shows nothing; the panel is then offered under「添加资源」 for signing in.
3
+ export function panel(limits) {
4
+ if (!limits.known()) return { items: [] };
5
+ const windows = limits.quota();
6
+ // The account the windows are of, compared across machines by Coda's group view.
7
+ return { items: [{ type: 'card', title: 'Claude SDK', ...(limits.account ? { identity: `claude:${limits.account}` } : {}), items: windows.length
8
+ ? windows.map(w => ({ type: 'meter', label: w.label, value: `剩余 ${w.remainingPercent}%`, fraction: w.remainingPercent / 100, time: w.resetsAt, timeLabel: '重置' }))
9
+ // Past its reset a window is not known, neither full nor empty: what was used since is not seen from here.
10
+ : [{ type: 'error', text: '额度记录已过期,会话再运行后更新' }] }] };
11
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # @coda-console/claude
2
-
3
- Bootstrap placeholder. The usable release will be published by coda-console/coda-plugins.