Files
Ivor StrelarandClaude Opus 5 0c3830097b feat: add SF Claude Code node
Runs prompts through the locally installed Claude Code CLI, so it uses the
user's existing Claude login rather than an Anthropic API key. Accepts a
system prompt and up to 16 images, which are written to a temp working
directory and read back by the agent's Read tool.

Image inputs grow on demand: connecting one reveals the next empty slot, and
unused trailing slots are trimmed.

Closes stdin on the subprocess, which otherwise costs 3s per call while the
CLI waits for input that never arrives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 08:50:24 +02:00

86 lines
2.8 KiB
JavaScript

/**
* SF Claude Code — grows the image input list on demand.
*
* The Python side declares image_1..image_16 as optional inputs. This keeps
* only as many slots visible as are in use, plus one spare to connect into.
*/
import { app } from "../../../scripts/app.js";
const NODE_NAME = "SFClaudeCode";
const MAX_IMAGES = 16;
function imageSlotIndex(node, n) {
return (node.inputs ?? []).findIndex((input) => input.name === `image_${n}`);
}
function syncImageSlots(node) {
if (!node.inputs) {
return;
}
// Highest numbered image slot that currently has something plugged in.
let lastUsed = 0;
for (const input of node.inputs) {
const match = input.name?.match(/^image_(\d+)$/);
if (match && input.link != null) {
lastUsed = Math.max(lastUsed, parseInt(match[1], 10));
}
}
// Show every used slot, plus one empty spare to drag into.
const wanted = Math.min(lastUsed + 1, MAX_IMAGES);
for (let n = 1; n <= wanted; n++) {
if (imageSlotIndex(node, n) === -1) {
node.addInput(`image_${n}`, "IMAGE");
}
}
// Drop trailing spares, but never one that is still connected.
for (let n = MAX_IMAGES; n > wanted; n--) {
const index = imageSlotIndex(node, n);
if (index !== -1 && node.inputs[index].link == null) {
node.removeInput(index);
}
}
node.setSize(node.computeSize());
node.setDirtyCanvas(true, true);
}
app.registerExtension({
name: "Stillfront.ClaudeCode",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== NODE_NAME) {
return;
}
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
syncImageSlots(this);
return result;
};
// Re-sync after a saved workflow restores its connections.
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
const result = onConfigure ? onConfigure.apply(this, arguments) : undefined;
requestAnimationFrame(() => syncImageSlots(this));
return result;
};
const onConnectionsChange = nodeType.prototype.onConnectionsChange;
nodeType.prototype.onConnectionsChange = function (type, index, connected, linkInfo, ioSlot) {
const result = onConnectionsChange
? onConnectionsChange.apply(this, arguments)
: undefined;
// Let litegraph finish updating link state before recounting.
requestAnimationFrame(() => syncImageSlots(this));
return result;
};
},
});