history: Saturday, 2 August 2026 — Claude API + Claude Code providers for rojo-chat-panel
- Claude API provider (@anthropic-ai/sdk): streaming, separate session history, tool use support, rojo-settings-panel claude endpoint type - Claude Code subprocess provider: --print --output-format stream-json --verbose, --session-id/--resume multi-turn, shell:false Windows fix, rojo-settings-panel claude-code endpoint type (no API key) - Rojo local-mode fixes: detection, creation, system-prompt delivery - Three subprocess bugs fixed: missing --verbose, stdin blocking, cmd.exe mangling Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
87a42fd5a5
commit
6766f6d467
|
|
@ -8,6 +8,8 @@
|
|||
"name": "roject",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-code": "^2.1.220",
|
||||
"@anthropic-ai/sdk": "^0.115.0",
|
||||
"@langchain/core": "^1.2.2",
|
||||
"@langchain/openai": "^1.5.4",
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
|
|
@ -28,6 +30,163 @@
|
|||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-code": {
|
||||
"version": "2.1.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-2.1.220.tgz",
|
||||
"integrity": "sha512-ogBrvwkqF9f8okmnXKxmRNHuvtFxFEffe5pWdqOV3iQDxlUOKirFqnyWC7NGXXnDA4WkkbPH8pvSbwyCR2Auyw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "SEE LICENSE IN README.md",
|
||||
"bin": {
|
||||
"claude": "bin/claude.exe"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@anthropic-ai/claude-code-darwin-arm64": "2.1.220",
|
||||
"@anthropic-ai/claude-code-darwin-x64": "2.1.220",
|
||||
"@anthropic-ai/claude-code-linux-arm64": "2.1.220",
|
||||
"@anthropic-ai/claude-code-linux-arm64-musl": "2.1.220",
|
||||
"@anthropic-ai/claude-code-linux-x64": "2.1.220",
|
||||
"@anthropic-ai/claude-code-linux-x64-musl": "2.1.220",
|
||||
"@anthropic-ai/claude-code-win32-arm64": "2.1.220",
|
||||
"@anthropic-ai/claude-code-win32-x64": "2.1.220"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-code-darwin-arm64": {
|
||||
"version": "2.1.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-darwin-arm64/-/claude-code-darwin-arm64-2.1.220.tgz",
|
||||
"integrity": "sha512-rmtd41Bf+n+YnhjSjtQ8WG5qy8KKogUp3YRfQrkLsTgPUD0H3j869rBInBJT3SHrKQ0hLghQLGM73CC1C+USLQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-code-darwin-x64": {
|
||||
"version": "2.1.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-darwin-x64/-/claude-code-darwin-x64-2.1.220.tgz",
|
||||
"integrity": "sha512-hbuoG+YCo37VzSKzKJ47ymRmt/YjASc3dRcsZtCcftLYdopv8KL889x/IbCl3cfp/VqV2rRDZ0f3aUDpHUFweQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-code-linux-arm64": {
|
||||
"version": "2.1.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-linux-arm64/-/claude-code-linux-arm64-2.1.220.tgz",
|
||||
"integrity": "sha512-VHFI8mKruIntKn7eq81sbyS19/KWmQcmJQsS/C+j9M/E+w0s4UytgsL7DADPjBE/GByNiKoRtLYDMntCjRlOdA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-code-linux-arm64-musl": {
|
||||
"version": "2.1.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-linux-arm64-musl/-/claude-code-linux-arm64-musl-2.1.220.tgz",
|
||||
"integrity": "sha512-m37ALw8jcbSknuyG7xDQjGPY7Gth3eX8iFY1XFEWABVq1iUMVAUn96WC9eqwi8/JSqyG2t3oNRiqHdi2ZNKFGQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-code-linux-x64": {
|
||||
"version": "2.1.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-linux-x64/-/claude-code-linux-x64-2.1.220.tgz",
|
||||
"integrity": "sha512-3CGFCnI0gpgsqNeJruFALBDGJaKXOuok3alQEg56ty2yOPpIrOx/r2Y0+T4uhJl7kP5Hzw4IFkxo4DZKWvzQ7Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-code-linux-x64-musl": {
|
||||
"version": "2.1.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-linux-x64-musl/-/claude-code-linux-x64-musl-2.1.220.tgz",
|
||||
"integrity": "sha512-+QyT1KikOdMRKReWFaBYGsroYx2vEjjx54DwhMoC24oE1DxjC+SlKjeOTRXAKiu0fr0O549Lkhg2tuT5xtQpAQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-code-win32-arm64": {
|
||||
"version": "2.1.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-win32-arm64/-/claude-code-win32-arm64-2.1.220.tgz",
|
||||
"integrity": "sha512-APqZwFBn38DBUwB65uUTetW7lbtUqFfAfOWKvkmOyqFDswDEsInaINuIwqMCl44WYcch10SaHhEZdXJU9MG3aQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-code-win32-x64": {
|
||||
"version": "2.1.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code-win32-x64/-/claude-code-win32-x64-2.1.220.tgz",
|
||||
"integrity": "sha512-UGrjH8cGhC6PzhTyZSdgf/RpKxpfk9XJZ/RT/wsG2AJg9yEJLjLg6/TrnlL8RFbEv6Zahu0Quytc02UOpA/GiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.115.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.115.0.tgz",
|
||||
"integrity": "sha512-BJrFIVyjNuU8lfDyIJTvlRYzgQg+zEl78BxE7fq8esULsGz9IRQvGtW5spq3tydmtjQb/GFdooKGdGsetpx+lQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
"standardwebhooks": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"anthropic-ai-sdk": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cfworker/json-schema": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz",
|
||||
|
|
@ -892,6 +1051,12 @@
|
|||
"url": "https://github.com/sindresorhus/is?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@stablelib/base64": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
||||
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
|
|
@ -2907,6 +3072,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-sha256": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
|
||||
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
|
||||
|
|
@ -3616,6 +3787,19 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-to-ts": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
|
||||
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"ts-algebra": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
|
|
@ -5072,6 +5256,16 @@
|
|||
"license": "BSD-3-Clause",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/standardwebhooks": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
|
||||
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stablelib/base64": "^1.0.0",
|
||||
"fast-sha256": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stat-mode": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz",
|
||||
|
|
@ -5353,6 +5547,12 @@
|
|||
"utf8-byte-length": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-algebra": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
|
||||
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
|
|
@ -5728,6 +5928,7 @@
|
|||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||||
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
"electron:dist": "npm run electron:build && electron-builder"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-code": "^2.1.220",
|
||||
"@anthropic-ai/sdk": "^0.115.0",
|
||||
"@langchain/core": "^1.2.2",
|
||||
"@langchain/openai": "^1.5.4",
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
|
|
|
|||
|
|
@ -148,12 +148,26 @@ class RojoChatPanel extends HTMLElement
|
|||
|
||||
async _loadRojos(): Promise<void>
|
||||
{
|
||||
const projectId = Editor.get().projectId;
|
||||
if ( !projectId ) return;
|
||||
const editor = Editor.get();
|
||||
let url: string;
|
||||
|
||||
if ( editor.localRoot )
|
||||
{
|
||||
url = `/api/local/rojos?root=${ encodeURIComponent( editor.localRoot ) }`;
|
||||
}
|
||||
else if ( editor.remoteProject )
|
||||
{
|
||||
url = `/api/remote/rojos/${ editor.remoteProject }/list`;
|
||||
}
|
||||
else if ( editor.projectId )
|
||||
{
|
||||
url = `/api/rojos/${ editor.projectId }/list`;
|
||||
}
|
||||
else { return; }
|
||||
|
||||
try
|
||||
{
|
||||
const res = await fetch( `/api/rojos/${ projectId }/list` );
|
||||
const res = await fetch( url );
|
||||
if ( !res.ok ) return;
|
||||
this._rojos = await res.json() as RojoEntry[];
|
||||
this._renderPicker();
|
||||
|
|
@ -184,22 +198,39 @@ class RojoChatPanel extends HTMLElement
|
|||
|
||||
async _createRojo(): Promise<void>
|
||||
{
|
||||
const projectId = Editor.get().projectId;
|
||||
if ( !projectId ) return;
|
||||
const editor = Editor.get();
|
||||
|
||||
// Place new rojo in the same directory as the currently selected one,
|
||||
// or directly in workspace/rojos if nothing is selected.
|
||||
const dir = this._selectedPath
|
||||
? parentDir( this._selectedPath )
|
||||
: 'workspace/rojos';
|
||||
|
||||
let url: string;
|
||||
let body: Record<string, string>;
|
||||
|
||||
if ( editor.localRoot )
|
||||
{
|
||||
url = '/api/local/rojos/create';
|
||||
body = { root: editor.localRoot, parentDir: dir };
|
||||
}
|
||||
else if ( editor.remoteProject )
|
||||
{
|
||||
url = `/api/remote/rojos/${ editor.remoteProject }/create`;
|
||||
body = { parentDir: dir };
|
||||
}
|
||||
else if ( editor.projectId )
|
||||
{
|
||||
url = `/api/rojos/${ editor.projectId }/create`;
|
||||
body = { parentDir: dir };
|
||||
}
|
||||
else { return; }
|
||||
|
||||
try
|
||||
{
|
||||
const res = await fetch( `/api/rojos/${ projectId }/create`,
|
||||
const res = await fetch( url,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify( { parentDir: dir } ),
|
||||
body: JSON.stringify( body ),
|
||||
} );
|
||||
|
||||
if ( !res.ok ) return;
|
||||
|
|
@ -209,7 +240,7 @@ class RojoChatPanel extends HTMLElement
|
|||
this._selectedPath = newPath;
|
||||
( this.querySelector( '.rcp-picker' ) as HTMLSelectElement ).value = newPath;
|
||||
|
||||
await Editor.get().openDocument( newPath );
|
||||
await editor.openDocument( newPath );
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
|
@ -267,8 +298,12 @@ class RojoChatPanel extends HTMLElement
|
|||
|
||||
if ( this._selectedPath )
|
||||
{
|
||||
body.projectId = Editor.get().projectId;
|
||||
body.rojoPath = this._selectedPath;
|
||||
const editor = Editor.get();
|
||||
body.rojoPath = this._selectedPath;
|
||||
|
||||
if ( editor.localRoot ) { body.localRoot = editor.localRoot; }
|
||||
else if ( editor.remoteProject ) { body.remoteProject = editor.remoteProject; }
|
||||
else { body.projectId = editor.projectId; }
|
||||
}
|
||||
|
||||
const response = await fetch( '/api/rojos/chat',
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ function inkscapeLabel( el: Element ): string
|
|||
|
||||
interface RojoEndpoint
|
||||
{
|
||||
type: 'external' | 'tunnel';
|
||||
type: 'external' | 'tunnel' | 'claude' | 'claude-code';
|
||||
url: string;
|
||||
model: string;
|
||||
apiKey: string;
|
||||
|
|
@ -159,12 +159,16 @@ class RojoSettingsPanel extends HTMLElement
|
|||
<select class="rsp-input" data-field="endpoint.type">
|
||||
<option value="external">External / Local URL</option>
|
||||
<option value="tunnel">Tunnel (tunnel.rokojori.com)</option>
|
||||
<option value="claude">Claude API (Anthropic)</option>
|
||||
<option value="claude-code">Claude Code (local, no key)</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="rsp-endpoint-external">
|
||||
<label class="rsp-label">Base URL
|
||||
<input class="rsp-input" data-field="endpoint.url" type="text" placeholder="http://localhost:11434/v1">
|
||||
</label>
|
||||
<div class="rsp-url-row">
|
||||
<label class="rsp-label">Base URL
|
||||
<input class="rsp-input" data-field="endpoint.url" type="text" placeholder="http://localhost:11434/v1">
|
||||
</label>
|
||||
</div>
|
||||
<label class="rsp-label">API Key
|
||||
<input class="rsp-input" data-field="endpoint.apiKey" type="password" placeholder="sk-… (or leave blank for local)">
|
||||
</label>
|
||||
|
|
@ -180,8 +184,16 @@ class RojoSettingsPanel extends HTMLElement
|
|||
<div class="rsp-tunnel-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="rsp-label">Model
|
||||
<input class="rsp-input" data-field="endpoint.model" type="text" placeholder="gpt-4o / llama3 / gemma3 / …">
|
||||
<div class="rsp-endpoint-claude-code" style="display:none">
|
||||
<div class="rsp-claude-code-info">Uses the local Claude Code CLI — no API key needed. Inherits your existing Claude Code login.</div>
|
||||
</div>
|
||||
<label class="rsp-label rsp-model-row">Model
|
||||
<input class="rsp-input" data-field="endpoint.model" type="text" list="rsp-model-suggestions" placeholder="gpt-4o / llama3 / claude-opus-4-8 / …">
|
||||
<datalist id="rsp-model-suggestions">
|
||||
<option value="claude-opus-4-8">
|
||||
<option value="claude-sonnet-4-6">
|
||||
<option value="claude-haiku-4-5">
|
||||
</datalist>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -337,7 +349,7 @@ class RojoSettingsPanel extends HTMLElement
|
|||
description: this._getField( 'description' ),
|
||||
systemPrompt: this._getField( 'systemPrompt' ),
|
||||
endpoint: {
|
||||
type: this._getField( 'endpoint.type' ) as 'external' | 'tunnel',
|
||||
type: this._getField( 'endpoint.type' ) as 'external' | 'tunnel' | 'claude' | 'claude-code',
|
||||
url: this._getField( 'endpoint.url' ),
|
||||
apiKey: this._getField( 'endpoint.apiKey' ),
|
||||
tunnelId: this._getField( 'endpoint.tunnelId' ),
|
||||
|
|
@ -356,9 +368,15 @@ class RojoSettingsPanel extends HTMLElement
|
|||
|
||||
_updateEndpointVisibility(): void
|
||||
{
|
||||
const type = this._getField( 'endpoint.type' );
|
||||
( this.querySelector( '.rsp-endpoint-external' ) as HTMLElement ).style.display = type === 'external' ? '' : 'none';
|
||||
( this.querySelector( '.rsp-endpoint-tunnel' ) as HTMLElement ).style.display = type === 'tunnel' ? '' : 'none';
|
||||
const type = this._getField( 'endpoint.type' );
|
||||
const isCC = type === 'claude-code';
|
||||
( this.querySelector( '.rsp-endpoint-external' ) as HTMLElement ).style.display = ( type === 'external' || type === 'claude' ) ? '' : 'none';
|
||||
( this.querySelector( '.rsp-endpoint-tunnel' ) as HTMLElement ).style.display = type === 'tunnel' ? '' : 'none';
|
||||
( this.querySelector( '.rsp-endpoint-claude-code' ) as HTMLElement ).style.display = isCC ? '' : 'none';
|
||||
const urlRow = this.querySelector( '.rsp-url-row' ) as HTMLElement | null;
|
||||
const modelRow = this.querySelector( '.rsp-model-row' ) as HTMLElement | null;
|
||||
if ( urlRow ) urlRow.style.display = type === 'claude' ? 'none' : '';
|
||||
if ( modelRow ) modelRow.style.display = isCC ? 'none' : '';
|
||||
}
|
||||
|
||||
async _syncFilename(): Promise<void>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
import Anthropic from "@anthropic-ai/sdk";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { HumanMessage, AIMessage, SystemMessage, ToolMessage, BaseMessage } from "@langchain/core/messages";
|
||||
import { spawn } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { RojoToolContext } from "./tools/RojoTool.js";
|
||||
|
||||
export interface AgentConfig
|
||||
{
|
||||
baseURL: string;
|
||||
model: string;
|
||||
apiKey: string;
|
||||
headers?: Record<string, string>;
|
||||
baseURL?: string;
|
||||
model: string;
|
||||
apiKey: string;
|
||||
provider?: "openai" | "claude" | "claude-code";
|
||||
headers?: Record<string, string>;
|
||||
systemPrompt?: string;
|
||||
tools?: object[];
|
||||
tools?: object[];
|
||||
}
|
||||
|
||||
export type AgentEvent =
|
||||
|
|
@ -19,18 +24,50 @@ export type AgentEvent =
|
|||
|
||||
type ToolExecutor = ( name: string, args: Record<string, unknown>, ctx: RojoToolContext ) => Promise<unknown>;
|
||||
|
||||
type SessionData = { id: string; messages: BaseMessage[] };
|
||||
type SessionData = { id: string; messages: BaseMessage[] };
|
||||
type ClaudeSessionData = { id: string; messages: Anthropic.MessageParam[] };
|
||||
|
||||
const sessions = new Map<string, SessionData>();
|
||||
const sessions = new Map<string, SessionData>();
|
||||
const claudeSessions = new Map<string, ClaudeSessionData>();
|
||||
const claudeCodeSessions = new Set<string>();
|
||||
|
||||
// ── Claude binary path ───────────────────────────────────────────────────────
|
||||
|
||||
function findClaudeBin(): string
|
||||
{
|
||||
const win = process.platform === "win32";
|
||||
const direct = path.join( process.cwd(), "node_modules", "@anthropic-ai", "claude-code", "bin", win ? "claude.exe" : "claude" );
|
||||
if ( fs.existsSync( direct ) ) return direct;
|
||||
const wrapper = path.join( process.cwd(), "node_modules", ".bin", win ? "claude.cmd" : "claude" );
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
const CLAUDE_BIN = findClaudeBin();
|
||||
|
||||
// ── Main entry ───────────────────────────────────────────────────────────────
|
||||
|
||||
export async function* getAgentStream(
|
||||
id: string,
|
||||
userMessage: string,
|
||||
config: AgentConfig,
|
||||
toolContext?: RojoToolContext,
|
||||
id: string,
|
||||
userMessage: string,
|
||||
config: AgentConfig,
|
||||
toolContext?: RojoToolContext,
|
||||
toolExecutor?: ToolExecutor,
|
||||
): AsyncGenerator<AgentEvent>
|
||||
{
|
||||
if ( config.provider === "claude" )
|
||||
{
|
||||
yield* getClaudeAgentStream( id, userMessage, config, toolContext, toolExecutor );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( config.provider === "claude-code" )
|
||||
{
|
||||
yield* getClaudeCodeAgentStream( id, userMessage, config );
|
||||
return;
|
||||
}
|
||||
|
||||
// ── OpenAI-compatible path ─────────────────────────────────────────────────
|
||||
|
||||
const model = new ChatOpenAI(
|
||||
{
|
||||
model: config.model,
|
||||
|
|
@ -119,6 +156,240 @@ export async function* getAgentStream(
|
|||
}
|
||||
}
|
||||
|
||||
// ── Claude API provider ──────────────────────────────────────────────────────
|
||||
|
||||
function toClaudeTool( t: any ): Anthropic.Tool
|
||||
{
|
||||
const fn = t.function ?? t;
|
||||
return {
|
||||
name: fn.name,
|
||||
description: fn.description ?? "",
|
||||
input_schema: fn.parameters as Anthropic.Tool[ "input_schema" ],
|
||||
};
|
||||
}
|
||||
|
||||
async function* getClaudeAgentStream(
|
||||
id: string,
|
||||
userMessage: string,
|
||||
config: AgentConfig,
|
||||
toolContext?: RojoToolContext,
|
||||
toolExecutor?: ToolExecutor,
|
||||
): AsyncGenerator<AgentEvent>
|
||||
{
|
||||
const client = new Anthropic( { apiKey: config.apiKey } );
|
||||
|
||||
if ( !claudeSessions.has( id ) )
|
||||
{
|
||||
claudeSessions.set( id, { id, messages: [] } );
|
||||
}
|
||||
|
||||
const session = claudeSessions.get( id )!;
|
||||
const claudeTools = ( config.tools ?? [] ).map( toClaudeTool );
|
||||
|
||||
session.messages.push( { role: "user", content: userMessage } );
|
||||
|
||||
while ( true )
|
||||
{
|
||||
const streamParams: Anthropic.MessageStreamParams = {
|
||||
model: config.model,
|
||||
max_tokens: 8192,
|
||||
messages: session.messages,
|
||||
...( config.systemPrompt?.trim() ? { system: config.systemPrompt } : {} ),
|
||||
...( claudeTools.length > 0 ? { tools: claudeTools } : {} ),
|
||||
};
|
||||
|
||||
const stream = client.messages.stream( streamParams );
|
||||
|
||||
for await ( const event of stream )
|
||||
{
|
||||
if ( event.type === "content_block_delta" && event.delta.type === "text_delta" )
|
||||
{
|
||||
yield { type: "text", text: event.delta.text };
|
||||
}
|
||||
}
|
||||
|
||||
const finalMsg = await stream.finalMessage();
|
||||
const toolBlocks = finalMsg.content.filter( b => b.type === "tool_use" ) as Anthropic.ToolUseBlock[];
|
||||
|
||||
console.log( "[RojosAgent/claude] stop_reason:", finalMsg.stop_reason, "| tool_calls:", toolBlocks.length );
|
||||
|
||||
if ( toolBlocks.length > 0 && toolContext && toolExecutor )
|
||||
{
|
||||
session.messages.push( { role: "assistant", content: finalMsg.content } );
|
||||
|
||||
const toolResults: Anthropic.ToolResultBlockParam[] = [];
|
||||
|
||||
for ( const tc of toolBlocks )
|
||||
{
|
||||
yield { type: "tool_call", name: tc.name, args: tc.input as Record<string, unknown> };
|
||||
|
||||
let result: unknown;
|
||||
|
||||
try
|
||||
{
|
||||
result = await toolExecutor( tc.name, tc.input as Record<string, unknown>, toolContext );
|
||||
}
|
||||
catch ( err )
|
||||
{
|
||||
result = { error: String( err ) };
|
||||
}
|
||||
|
||||
const resultStr = typeof result === "string" ? result : JSON.stringify( result );
|
||||
yield { type: "tool_result", name: tc.name, result: resultStr };
|
||||
|
||||
toolResults.push( { type: "tool_result", tool_use_id: tc.id, content: resultStr } );
|
||||
}
|
||||
|
||||
session.messages.push( { role: "user", content: toolResults } );
|
||||
}
|
||||
else
|
||||
{
|
||||
const fullText = finalMsg.content
|
||||
.filter( b => b.type === "text" )
|
||||
.map( b => ( b as Anthropic.TextBlock ).text )
|
||||
.join( "" );
|
||||
|
||||
session.messages.push( { role: "assistant", content: fullText } );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Claude Code provider (subprocess) ───────────────────────────────────────
|
||||
|
||||
async function* getClaudeCodeAgentStream(
|
||||
id: string,
|
||||
userMessage: string,
|
||||
config: AgentConfig,
|
||||
): AsyncGenerator<AgentEvent>
|
||||
{
|
||||
const isFirst = !claudeCodeSessions.has( id );
|
||||
claudeCodeSessions.add( id );
|
||||
|
||||
const args: string[] = [
|
||||
"--print",
|
||||
"--output-format", "stream-json",
|
||||
"--verbose",
|
||||
"--dangerously-skip-permissions",
|
||||
];
|
||||
|
||||
if ( isFirst )
|
||||
{
|
||||
args.push( "--session-id", id );
|
||||
if ( config.systemPrompt?.trim() )
|
||||
{
|
||||
args.push( "--append-system-prompt", config.systemPrompt );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
args.push( "--resume", id );
|
||||
}
|
||||
|
||||
args.push( userMessage );
|
||||
|
||||
console.log( "[RojosAgent/claude-code] spawning:", CLAUDE_BIN, args.slice( 0, 6 ).join( " " ) );
|
||||
|
||||
yield* spawnJsonLines( CLAUDE_BIN, args );
|
||||
}
|
||||
|
||||
async function* spawnJsonLines( bin: string, args: string[] ): AsyncGenerator<AgentEvent>
|
||||
{
|
||||
const queue: AgentEvent[] = [];
|
||||
const waiters: Array<() => void> = [];
|
||||
let done = false;
|
||||
let spawnError: Error | null = null;
|
||||
|
||||
function push( ev: AgentEvent ): void
|
||||
{
|
||||
queue.push( ev );
|
||||
if ( waiters.length ) waiters.shift()!();
|
||||
}
|
||||
|
||||
function finish( err?: Error ): void
|
||||
{
|
||||
done = true;
|
||||
spawnError = err ?? null;
|
||||
while ( waiters.length ) waiters.shift()!();
|
||||
}
|
||||
|
||||
const proc = spawn( bin, args, { cwd: process.cwd(), shell: false, stdio: [ "ignore", "pipe", "pipe" ] } );
|
||||
|
||||
let buf = "";
|
||||
|
||||
proc.stdout.on( "data", ( chunk: Buffer ) =>
|
||||
{
|
||||
buf += chunk.toString( "utf8" );
|
||||
|
||||
const lines = buf.split( "\n" );
|
||||
buf = lines.pop() ?? "";
|
||||
|
||||
for ( const line of lines )
|
||||
{
|
||||
const t = line.trim();
|
||||
if ( !t ) continue;
|
||||
|
||||
try
|
||||
{
|
||||
const msg = JSON.parse( t ) as any;
|
||||
const ev = claudeCodeEventToAgentEvent( msg );
|
||||
if ( ev ) push( ev );
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
} );
|
||||
|
||||
proc.stderr.on( "data", ( chunk: Buffer ) =>
|
||||
{
|
||||
console.error( "[RojosAgent/claude-code] stderr:", chunk.toString( "utf8" ).trim() );
|
||||
} );
|
||||
|
||||
proc.on( "error", ( err ) => finish( err ) );
|
||||
proc.on( "close", ( code ) =>
|
||||
{
|
||||
if ( code !== 0 ) console.warn( "[RojosAgent/claude-code] exited with code", code );
|
||||
finish();
|
||||
} );
|
||||
|
||||
while ( true )
|
||||
{
|
||||
if ( queue.length > 0 )
|
||||
{
|
||||
yield queue.shift()!;
|
||||
}
|
||||
else if ( done )
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
await new Promise<void>( resolve => waiters.push( resolve ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( spawnError ) throw spawnError;
|
||||
}
|
||||
|
||||
function claudeCodeEventToAgentEvent( msg: any ): AgentEvent | null
|
||||
{
|
||||
if ( msg.type === "assistant" )
|
||||
{
|
||||
const content = msg.message?.content;
|
||||
if ( !Array.isArray( content ) ) return null;
|
||||
|
||||
const text = content
|
||||
.filter( ( b: any ) => b.type === "text" )
|
||||
.map( ( b: any ) => b.text as string )
|
||||
.join( "" );
|
||||
|
||||
return text ? { type: "text", text } : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Utilities ────────────────────────────────────────────────────────────────
|
||||
|
||||
function extractText( content: unknown ): string
|
||||
{
|
||||
if ( typeof content === "string" ) return content;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Router } from 'express';
|
|||
import { requireAuth } from '../../auth-connector/source/server/auth';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
|
||||
const router = Router();
|
||||
router.use( requireAuth );
|
||||
|
|
@ -114,4 +115,125 @@ router.post( '/delete', ( req, res ) =>
|
|||
res.json( { ok: true } );
|
||||
} );
|
||||
|
||||
// ── Rojo helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
function pickRandom<T>( arr: T[] ): T { return arr[ Math.floor( Math.random() * arr.length ) ]; }
|
||||
function capitalize( s: string ): string { return s[ 0 ].toUpperCase() + s.slice( 1 ); }
|
||||
|
||||
const ROJO_ADJECTIVES = [
|
||||
"funky", "cool", "groovy", "wild", "sharp", "slick", "swift", "bold",
|
||||
"bright", "calm", "daring", "eager", "fierce", "jolly", "lively",
|
||||
"quirky", "smart", "vivid", "zesty", "sleek", "crisp", "nifty", "rad",
|
||||
"sunny", "sassy", "snappy", "peppy", "zippy", "breezy", "mellow",
|
||||
];
|
||||
|
||||
const ROJO_ROLES = [
|
||||
"boss", "doctor", "player", "teacher", "hunter", "builder", "maker",
|
||||
"dancer", "singer", "painter", "writer", "coder", "pilot", "chef",
|
||||
"sailor", "rider", "climber", "dreamer", "wanderer", "scout", "keeper",
|
||||
"helper", "guide", "mentor", "runner", "seeker", "ranger", "scholar",
|
||||
];
|
||||
|
||||
const ROJO_NAMES = [
|
||||
"Sofia", "Elena", "Lucas", "Mia", "Noah", "Emma", "Leon", "Lena",
|
||||
"Felix", "Anna", "Max", "Clara", "Julian", "Sara", "Lars", "Nina",
|
||||
"Tom", "Lara", "Erik", "Ida", "Hugo", "Vera", "Otto", "Maja",
|
||||
"Kai", "Maya", "Arjun", "Zara", "Aisha", "Omar", "Nala", "Ravi",
|
||||
"Yuki", "Kenji", "Amara", "Diego", "Mateo", "Jae", "Sora", "Kira",
|
||||
"Rio", "Zion", "Nova", "Leila", "Cyrus", "Noa", "Bao", "Mila",
|
||||
];
|
||||
|
||||
function generateLocalRojoName(): { display: string; slug: string }
|
||||
{
|
||||
const adj = pickRandom( ROJO_ADJECTIVES );
|
||||
const role = pickRandom( ROJO_ROLES );
|
||||
const name = pickRandom( ROJO_NAMES );
|
||||
return {
|
||||
display: `${ capitalize( adj ) } ${ capitalize( role ) } ${ name }`,
|
||||
slug: `${ adj }-${ role }-${ name.toLowerCase() }`,
|
||||
};
|
||||
}
|
||||
|
||||
function scanLocalRojoFiles( dir: string, rootDir: string, out: string[] = [] ): string[]
|
||||
{
|
||||
if ( !fs.existsSync( dir ) ) return out;
|
||||
|
||||
for ( const entry of fs.readdirSync( dir, { withFileTypes: true } ) )
|
||||
{
|
||||
const full = path.join( dir, entry.name );
|
||||
if ( entry.isDirectory() ) { scanLocalRojoFiles( full, rootDir, out ); }
|
||||
else if ( entry.name.endsWith( '.rojo' ) )
|
||||
{
|
||||
out.push( path.relative( rootDir, full ).replace( /\\/g, '/' ) );
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── Local rojo list ──────────────────────────────────────────────────────────
|
||||
|
||||
router.get( '/rojos', ( req, res ) =>
|
||||
{
|
||||
const root = req.query.root as string;
|
||||
if ( !root ) { res.status( 400 ).json( { error: 'root required' } ); return; }
|
||||
|
||||
const resolvedRoot = path.resolve( root );
|
||||
const rojoDir = path.join( resolvedRoot, 'workspace', 'rojos' );
|
||||
const files = scanLocalRojoFiles( rojoDir, resolvedRoot );
|
||||
|
||||
const rojos = files.map( filePath =>
|
||||
{
|
||||
let id = '', name = filePath, description = '';
|
||||
const full = safeResolve( root, filePath );
|
||||
|
||||
if ( full && fs.existsSync( full ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
const s = JSON.parse( fs.readFileSync( full, 'utf8' ) );
|
||||
id = s.id || '';
|
||||
name = s.name || name;
|
||||
description = s.description || '';
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
return { id, name, description, path: filePath };
|
||||
} );
|
||||
|
||||
res.json( rojos );
|
||||
} );
|
||||
|
||||
// ── Local rojo create ────────────────────────────────────────────────────────
|
||||
|
||||
router.post( '/rojos/create', ( req, res ) =>
|
||||
{
|
||||
const { root, parentDir: parentDirArg } = req.body as { root: string; parentDir?: string };
|
||||
if ( !root ) { res.status( 400 ).json( { error: 'root required' } ); return; }
|
||||
|
||||
const dir = parentDirArg || 'workspace/rojos';
|
||||
const { display, slug } = generateLocalRojoName();
|
||||
const id = crypto.randomUUID();
|
||||
const filePath = `${ dir }/${ slug }.rojo`;
|
||||
const full = safeResolve( root, filePath );
|
||||
|
||||
if ( !full ) { res.status( 403 ).json( { error: 'Invalid path' } ); return; }
|
||||
|
||||
const defaults = JSON.stringify(
|
||||
{
|
||||
id,
|
||||
name: display,
|
||||
description: '',
|
||||
systemPrompt: '',
|
||||
endpoint: { type: 'external', url: '', model: '', apiKey: '', tunnelId: '' },
|
||||
appearance: { colors: [], layers: [] },
|
||||
}, null, 2 );
|
||||
|
||||
fs.mkdirSync( path.dirname( full ), { recursive: true } );
|
||||
fs.writeFileSync( full, defaults, 'utf8' );
|
||||
|
||||
res.json( { id, path: filePath, name: display } );
|
||||
} );
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -192,12 +192,13 @@ router.post( "/:projectId/create", ( req, res ) =>
|
|||
|
||||
router.post( "/chat", async ( req, res ) =>
|
||||
{
|
||||
const { id, message, projectId, rojoPath } = req.body as
|
||||
const { id, message, projectId, rojoPath, localRoot } = req.body as
|
||||
{
|
||||
id: string;
|
||||
message: string;
|
||||
projectId?: string;
|
||||
rojoPath?: string;
|
||||
id: string;
|
||||
message: string;
|
||||
projectId?: string;
|
||||
rojoPath?: string;
|
||||
localRoot?: string;
|
||||
};
|
||||
|
||||
if ( !id || !message )
|
||||
|
|
@ -214,9 +215,23 @@ router.post( "/chat", async ( req, res ) =>
|
|||
tools: projectId ? RojoToolRegistry.definitions() : [],
|
||||
};
|
||||
|
||||
if ( projectId && rojoPath )
|
||||
if ( rojoPath )
|
||||
{
|
||||
const content = readProjectFile( projectId, rojoPath );
|
||||
let content: string | null = null;
|
||||
|
||||
if ( localRoot )
|
||||
{
|
||||
const resolvedRoot = path.resolve( localRoot );
|
||||
const full = path.resolve( path.join( resolvedRoot, rojoPath ) );
|
||||
if ( full.startsWith( resolvedRoot + path.sep ) && fs.existsSync( full ) )
|
||||
{
|
||||
content = fs.readFileSync( full, "utf8" );
|
||||
}
|
||||
}
|
||||
else if ( projectId )
|
||||
{
|
||||
content = readProjectFile( projectId, rojoPath );
|
||||
}
|
||||
|
||||
if ( content )
|
||||
{
|
||||
|
|
@ -227,7 +242,17 @@ router.post( "/chat", async ( req, res ) =>
|
|||
|
||||
config.systemPrompt = settings.systemPrompt ?? "";
|
||||
|
||||
if ( ep.type === "tunnel" && ep.tunnelId )
|
||||
if ( ep.type === "claude-code" )
|
||||
{
|
||||
config.provider = "claude-code";
|
||||
}
|
||||
else if ( ep.type === "claude" )
|
||||
{
|
||||
config.provider = "claude";
|
||||
config.model = ep.model || "claude-opus-4-8";
|
||||
config.apiKey = ep.apiKey || "";
|
||||
}
|
||||
else if ( ep.type === "tunnel" && ep.tunnelId )
|
||||
{
|
||||
const tunnelServer = process.env.TUNNEL_SERVER_URL || "https://tunnel.rokojori.com";
|
||||
config.baseURL = `${ tunnelServer }/t/${ ep.tunnelId }/v1`;
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ var NAV_DATA = {
|
|||
title: 'History',
|
||||
path: 'history/index.html',
|
||||
children: [
|
||||
{ title: 'Saturday, 2 August 2026', path: 'history/2026/08-August/02-Saturday/index.html' },
|
||||
{ title: 'Friday, 31 July 2026', path: 'history/2026/07-July/31-Friday/index.html' },
|
||||
{ title: 'Thursday, 30 July 2026', path: 'history/2026/07-July/30-Thursday/index.html' },
|
||||
{ title: 'Friday, 24 July 2026', path: 'history/2026/07-July/24-Friday/index.html' },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,177 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Saturday, 2 August 2026 — Roject</title>
|
||||
<link rel="stylesheet" href="../../../../_assets_/styles.css">
|
||||
<link rel="stylesheet" href="../../../../_assets_/nav.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
|
||||
<header>
|
||||
<p class="date">Saturday, 2 August 2026</p>
|
||||
<h1>Session History</h1>
|
||||
<p class="subtitle">Rojo bug fixes (local mode detection, creation, system prompt). Claude API provider and Claude Code subprocess provider for rojo-chat-panel.</p>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<h2>What we built</h2>
|
||||
|
||||
<div class="card">
|
||||
<h3>Rojo local-mode bug fixes (three)</h3>
|
||||
<p>
|
||||
All three bugs shared the same root cause: <code>rojo-chat-panel</code> only checked
|
||||
<code>editor.projectId</code>, silently ignoring <code>localRoot</code> and
|
||||
<code>remoteProject</code> modes.
|
||||
</p>
|
||||
<ul style="margin-top:0.5rem;line-height:1.9">
|
||||
<li><strong>Detection</strong> — <code>_loadRojos()</code> now uses a three-way branch
|
||||
matching <code>Editor.ts</code>'s three mode fields, hitting
|
||||
<code>/api/local/rojos?root=…</code> for local projects.</li>
|
||||
<li><strong>Creation</strong> — <code>_createRojo()</code> received the same fix,
|
||||
posting to <code>/api/local/rojos/create</code> with <code>{ root, parentDir }</code>.</li>
|
||||
<li><strong>System prompt</strong> — <code>_send()</code> was sending
|
||||
<code>body.projectId = ''</code> (falsy), so the server skipped reading the rojo file.
|
||||
Fixed by sending <code>localRoot</code> in the body and branching server-side
|
||||
with a path-containment security check.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Claude API provider (<code>provider: "claude"</code>)</h3>
|
||||
<p>
|
||||
Installed <code>@anthropic-ai/sdk</code> and added a second LLM path to
|
||||
<code>RojosAgent.ts</code>:
|
||||
</p>
|
||||
<ul style="margin-top:0.5rem;line-height:1.9">
|
||||
<li><code>AgentConfig</code> extended with optional <code>provider?: "openai" | "claude" | "claude-code"</code>
|
||||
and <code>baseURL</code> made optional.</li>
|
||||
<li>Separate <code>claudeSessions</code> map (<code>Anthropic.MessageParam[]</code>)
|
||||
keeps conversation history independently of the LangChain OpenAI sessions.</li>
|
||||
<li>Streaming via <code>client.messages.stream()</code>; text deltas yielded in
|
||||
real-time. After iteration, <code>stream.finalMessage()</code> is used to collect
|
||||
tool use blocks for the agentic loop.</li>
|
||||
<li>OpenAI-format tools converted to Claude format by <code>toClaudeTool()</code>
|
||||
(wraps <code>parameters</code> as <code>input_schema</code>).</li>
|
||||
<li>Route <code>rojos.ts</code>: <code>ep.type === "claude"</code> branch sets
|
||||
<code>config.provider</code>, defaults model to <code>claude-opus-4-8</code>.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Claude Code subprocess provider (<code>provider: "claude-code"</code>)</h3>
|
||||
<p>
|
||||
Adds a third provider path that spawns the local <code>claude.exe</code> binary
|
||||
(already installed via <code>@anthropic-ai/claude-code</code>) as a subprocess.
|
||||
No API key needed — reuses the existing Claude Code login from the system keychain.
|
||||
</p>
|
||||
<ul style="margin-top:0.5rem;line-height:1.9">
|
||||
<li>CLI flags: <code>--print --output-format stream-json --verbose
|
||||
--dangerously-skip-permissions</code>.</li>
|
||||
<li>First message uses <code>--session-id <conversationId></code> to create a
|
||||
persistent session; subsequent messages use <code>--resume <conversationId></code>
|
||||
to continue it — full multi-turn conversation.</li>
|
||||
<li>System prompt passed via <code>--append-system-prompt</code> to preserve Claude
|
||||
Code's own built-in instructions.</li>
|
||||
<li><code>spawnJsonLines()</code> is an async generator backed by an event queue;
|
||||
stdout is parsed line-by-line as newline-delimited JSON.
|
||||
<code>type === "assistant"</code> events are converted to <code>AgentEvent</code>.</li>
|
||||
<li>Binary located at runtime via <code>findClaudeBin()</code> (checks
|
||||
<code>node_modules/@anthropic-ai/claude-code/bin/claude.exe</code> first,
|
||||
falls back to <code>.bin/claude.cmd</code>).</li>
|
||||
</ul>
|
||||
<p style="margin-top:0.75rem">
|
||||
Three bugs fixed during initial testing:
|
||||
</p>
|
||||
<ul style="margin-top:0.5rem;line-height:1.9">
|
||||
<li><strong>Missing <code>--verbose</code></strong> — required by the CLI when
|
||||
<code>stream-json</code> output format is used; without it the process exits with
|
||||
an error immediately.</li>
|
||||
<li><strong>Stdin not closed</strong> — process waited 3 s for piped input before
|
||||
starting. Fixed by <code>stdio: ["ignore", "pipe", "pipe"]</code>.</li>
|
||||
<li><strong><code>shell: true</code> on Windows</strong> — cmd.exe mangled the user
|
||||
message (split on spaces, <code>?</code> treated as wildcard), so Claude received
|
||||
only a fragment. Fixed by <code>shell: false</code> with the direct <code>.exe</code>
|
||||
path, letting Node.js call <code>CreateProcess()</code> directly.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Rojo settings panel: <code>claude</code> and <code>claude-code</code> endpoint types</h3>
|
||||
<p>
|
||||
Two new options added to the endpoint type selector:
|
||||
</p>
|
||||
<ul style="margin-top:0.5rem;line-height:1.9">
|
||||
<li><strong>Claude API (Anthropic)</strong> — shows the API key field; hides the Base URL
|
||||
row (not needed). Model field stays visible with a <code><datalist></code>
|
||||
suggesting <code>claude-opus-4-8</code>, <code>claude-sonnet-4-6</code>,
|
||||
<code>claude-haiku-4-5</code>.</li>
|
||||
<li><strong>Claude Code (local, no key)</strong> — hides the external section and model
|
||||
row entirely; shows a brief info note explaining that the local Claude Code login
|
||||
is used.</li>
|
||||
</ul>
|
||||
<p style="margin-top:0.75rem">
|
||||
<code>_updateEndpointVisibility()</code> controls all show/hide logic including the
|
||||
<code>.rsp-url-row</code> sub-element inside the external section.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Key decisions</h2>
|
||||
|
||||
<div class="card">
|
||||
<p>
|
||||
<strong>Separate session stores per provider.</strong>
|
||||
The OpenAI path keeps <code>BaseMessage[]</code> (LangChain); the Claude API path
|
||||
keeps <code>Anthropic.MessageParam[]</code>; the Claude Code path keeps only a
|
||||
<code>Set<string></code> of initialised session IDs (history lives in the
|
||||
claude subprocess's own storage). Mixing them would require format translation on
|
||||
every turn.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<p>
|
||||
<strong><code>--append-system-prompt</code> instead of <code>--system-prompt</code>.</strong>
|
||||
<code>--system-prompt</code> replaces Claude Code's entire built-in prompt, which
|
||||
would break the subprocess's tool-use instructions. <code>--append-system-prompt</code>
|
||||
adds the rojo's persona on top without disturbing the base behaviour.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<p>
|
||||
<strong><code>shell: false</code> with direct <code>.exe</code> path on Windows.</strong>
|
||||
Using <code>shell: true</code> routes through <code>cmd.exe</code>, which interprets
|
||||
spaces as argument separators and <code>?</code> as a wildcard — making multi-word
|
||||
user messages arrive garbled. Direct process creation passes each argv element as a
|
||||
properly quoted string.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<p>
|
||||
<strong>Claude Code as a no-key Claude backend.</strong>
|
||||
Because the subprocess inherits the OS user's existing Claude Code login (keychain /
|
||||
<code>~/.claude/</code>), no Anthropic API key is required in the rojo settings.
|
||||
The Claude Code subprocess can also use its own full tool set (Read, Bash, Grep, etc.),
|
||||
making it more capable than the pure API path for code-aware tasks.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
Roject — session history
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
<script>var NAV_ROOT = '../../../../';</script>
|
||||
<script src="../../../../_assets_/nav-data.js"></script>
|
||||
<script src="../../../../_assets_/nav.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -16,6 +16,16 @@
|
|||
<p class="subtitle">Log of all working sessions, most recent first.</p>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<h2>2026 — August</h2>
|
||||
|
||||
<div class="card">
|
||||
<h3><a href="2026/08-August/02-Saturday/index.html">Saturday, 2 August 2026</a></h3>
|
||||
<p>Rojo local-mode bug fixes: detection, creation, and system-prompt delivery (all caused by the same missing localRoot branching). Claude API provider: <code>@anthropic-ai/sdk</code> streaming, separate conversation history, tool use support. Claude Code subprocess provider: no API key needed, <code>--print --output-format stream-json --verbose</code>, <code>--session-id</code>/<code>--resume</code> for multi-turn. Settings panel: <code>claude</code> and <code>claude-code</code> endpoint types. Three bugs fixed: missing <code>--verbose</code>, stdin blocking, and <code>shell: true</code> mangling messages on Windows.</p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>2026 — July</h2>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue