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",
|
"name": "roject",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@anthropic-ai/claude-code": "^2.1.220",
|
||||||
|
"@anthropic-ai/sdk": "^0.115.0",
|
||||||
"@langchain/core": "^1.2.2",
|
"@langchain/core": "^1.2.2",
|
||||||
"@langchain/openai": "^1.5.4",
|
"@langchain/openai": "^1.5.4",
|
||||||
"@types/cookie-parser": "^1.4.10",
|
"@types/cookie-parser": "^1.4.10",
|
||||||
|
|
@ -28,6 +30,163 @@
|
||||||
"typescript": "^5.3.3"
|
"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": {
|
"node_modules/@cfworker/json-schema": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz",
|
"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"
|
"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": {
|
"node_modules/@standard-schema/spec": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
|
|
@ -2907,6 +3072,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/fast-uri": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
|
||||||
|
|
@ -3616,6 +3787,19 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/json-schema-traverse": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
|
|
@ -5072,6 +5256,16 @@
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"optional": true
|
"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": {
|
"node_modules/stat-mode": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz",
|
||||||
|
|
@ -5353,6 +5547,12 @@
|
||||||
"utf8-byte-length": "^1.0.1"
|
"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": {
|
"node_modules/ts-node": {
|
||||||
"version": "10.9.2",
|
"version": "10.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||||||
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@
|
||||||
"electron:dist": "npm run electron:build && electron-builder"
|
"electron:dist": "npm run electron:build && electron-builder"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@anthropic-ai/claude-code": "^2.1.220",
|
||||||
|
"@anthropic-ai/sdk": "^0.115.0",
|
||||||
"@langchain/core": "^1.2.2",
|
"@langchain/core": "^1.2.2",
|
||||||
"@langchain/openai": "^1.5.4",
|
"@langchain/openai": "^1.5.4",
|
||||||
"@types/cookie-parser": "^1.4.10",
|
"@types/cookie-parser": "^1.4.10",
|
||||||
|
|
|
||||||
|
|
@ -148,12 +148,26 @@ class RojoChatPanel extends HTMLElement
|
||||||
|
|
||||||
async _loadRojos(): Promise<void>
|
async _loadRojos(): Promise<void>
|
||||||
{
|
{
|
||||||
const projectId = Editor.get().projectId;
|
const editor = Editor.get();
|
||||||
if ( !projectId ) return;
|
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
|
try
|
||||||
{
|
{
|
||||||
const res = await fetch( `/api/rojos/${ projectId }/list` );
|
const res = await fetch( url );
|
||||||
if ( !res.ok ) return;
|
if ( !res.ok ) return;
|
||||||
this._rojos = await res.json() as RojoEntry[];
|
this._rojos = await res.json() as RojoEntry[];
|
||||||
this._renderPicker();
|
this._renderPicker();
|
||||||
|
|
@ -184,22 +198,39 @@ class RojoChatPanel extends HTMLElement
|
||||||
|
|
||||||
async _createRojo(): Promise<void>
|
async _createRojo(): Promise<void>
|
||||||
{
|
{
|
||||||
const projectId = Editor.get().projectId;
|
const editor = Editor.get();
|
||||||
if ( !projectId ) return;
|
|
||||||
|
|
||||||
// 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
|
const dir = this._selectedPath
|
||||||
? parentDir( this._selectedPath )
|
? parentDir( this._selectedPath )
|
||||||
: 'workspace/rojos';
|
: '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
|
try
|
||||||
{
|
{
|
||||||
const res = await fetch( `/api/rojos/${ projectId }/create`,
|
const res = await fetch( url,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify( { parentDir: dir } ),
|
body: JSON.stringify( body ),
|
||||||
} );
|
} );
|
||||||
|
|
||||||
if ( !res.ok ) return;
|
if ( !res.ok ) return;
|
||||||
|
|
@ -209,7 +240,7 @@ class RojoChatPanel extends HTMLElement
|
||||||
this._selectedPath = newPath;
|
this._selectedPath = newPath;
|
||||||
( this.querySelector( '.rcp-picker' ) as HTMLSelectElement ).value = newPath;
|
( this.querySelector( '.rcp-picker' ) as HTMLSelectElement ).value = newPath;
|
||||||
|
|
||||||
await Editor.get().openDocument( newPath );
|
await editor.openDocument( newPath );
|
||||||
}
|
}
|
||||||
catch {}
|
catch {}
|
||||||
}
|
}
|
||||||
|
|
@ -267,8 +298,12 @@ class RojoChatPanel extends HTMLElement
|
||||||
|
|
||||||
if ( this._selectedPath )
|
if ( this._selectedPath )
|
||||||
{
|
{
|
||||||
body.projectId = Editor.get().projectId;
|
const editor = Editor.get();
|
||||||
body.rojoPath = this._selectedPath;
|
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',
|
const response = await fetch( '/api/rojos/chat',
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ function inkscapeLabel( el: Element ): string
|
||||||
|
|
||||||
interface RojoEndpoint
|
interface RojoEndpoint
|
||||||
{
|
{
|
||||||
type: 'external' | 'tunnel';
|
type: 'external' | 'tunnel' | 'claude' | 'claude-code';
|
||||||
url: string;
|
url: string;
|
||||||
model: string;
|
model: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
|
|
@ -159,12 +159,16 @@ class RojoSettingsPanel extends HTMLElement
|
||||||
<select class="rsp-input" data-field="endpoint.type">
|
<select class="rsp-input" data-field="endpoint.type">
|
||||||
<option value="external">External / Local URL</option>
|
<option value="external">External / Local URL</option>
|
||||||
<option value="tunnel">Tunnel (tunnel.rokojori.com)</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>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<div class="rsp-endpoint-external">
|
<div class="rsp-endpoint-external">
|
||||||
<label class="rsp-label">Base URL
|
<div class="rsp-url-row">
|
||||||
<input class="rsp-input" data-field="endpoint.url" type="text" placeholder="http://localhost:11434/v1">
|
<label class="rsp-label">Base URL
|
||||||
</label>
|
<input class="rsp-input" data-field="endpoint.url" type="text" placeholder="http://localhost:11434/v1">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<label class="rsp-label">API Key
|
<label class="rsp-label">API Key
|
||||||
<input class="rsp-input" data-field="endpoint.apiKey" type="password" placeholder="sk-… (or leave blank for local)">
|
<input class="rsp-input" data-field="endpoint.apiKey" type="password" placeholder="sk-… (or leave blank for local)">
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -180,8 +184,16 @@ class RojoSettingsPanel extends HTMLElement
|
||||||
<div class="rsp-tunnel-list"></div>
|
<div class="rsp-tunnel-list"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label class="rsp-label">Model
|
<div class="rsp-endpoint-claude-code" style="display:none">
|
||||||
<input class="rsp-input" data-field="endpoint.model" type="text" placeholder="gpt-4o / llama3 / gemma3 / …">
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -337,7 +349,7 @@ class RojoSettingsPanel extends HTMLElement
|
||||||
description: this._getField( 'description' ),
|
description: this._getField( 'description' ),
|
||||||
systemPrompt: this._getField( 'systemPrompt' ),
|
systemPrompt: this._getField( 'systemPrompt' ),
|
||||||
endpoint: {
|
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' ),
|
url: this._getField( 'endpoint.url' ),
|
||||||
apiKey: this._getField( 'endpoint.apiKey' ),
|
apiKey: this._getField( 'endpoint.apiKey' ),
|
||||||
tunnelId: this._getField( 'endpoint.tunnelId' ),
|
tunnelId: this._getField( 'endpoint.tunnelId' ),
|
||||||
|
|
@ -356,9 +368,15 @@ class RojoSettingsPanel extends HTMLElement
|
||||||
|
|
||||||
_updateEndpointVisibility(): void
|
_updateEndpointVisibility(): void
|
||||||
{
|
{
|
||||||
const type = this._getField( 'endpoint.type' );
|
const type = this._getField( 'endpoint.type' );
|
||||||
( this.querySelector( '.rsp-endpoint-external' ) as HTMLElement ).style.display = type === 'external' ? '' : 'none';
|
const isCC = type === 'claude-code';
|
||||||
( this.querySelector( '.rsp-endpoint-tunnel' ) as HTMLElement ).style.display = type === 'tunnel' ? '' : 'none';
|
( 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>
|
async _syncFilename(): Promise<void>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,20 @@
|
||||||
|
import Anthropic from "@anthropic-ai/sdk";
|
||||||
import { ChatOpenAI } from "@langchain/openai";
|
import { ChatOpenAI } from "@langchain/openai";
|
||||||
import { HumanMessage, AIMessage, SystemMessage, ToolMessage, BaseMessage } from "@langchain/core/messages";
|
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";
|
import { RojoToolContext } from "./tools/RojoTool.js";
|
||||||
|
|
||||||
export interface AgentConfig
|
export interface AgentConfig
|
||||||
{
|
{
|
||||||
baseURL: string;
|
baseURL?: string;
|
||||||
model: string;
|
model: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
headers?: Record<string, string>;
|
provider?: "openai" | "claude" | "claude-code";
|
||||||
|
headers?: Record<string, string>;
|
||||||
systemPrompt?: string;
|
systemPrompt?: string;
|
||||||
tools?: object[];
|
tools?: object[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AgentEvent =
|
export type AgentEvent =
|
||||||
|
|
@ -19,18 +24,50 @@ export type AgentEvent =
|
||||||
|
|
||||||
type ToolExecutor = ( name: string, args: Record<string, unknown>, ctx: RojoToolContext ) => Promise<unknown>;
|
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(
|
export async function* getAgentStream(
|
||||||
id: string,
|
id: string,
|
||||||
userMessage: string,
|
userMessage: string,
|
||||||
config: AgentConfig,
|
config: AgentConfig,
|
||||||
toolContext?: RojoToolContext,
|
toolContext?: RojoToolContext,
|
||||||
toolExecutor?: ToolExecutor,
|
toolExecutor?: ToolExecutor,
|
||||||
): AsyncGenerator<AgentEvent>
|
): 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(
|
const model = new ChatOpenAI(
|
||||||
{
|
{
|
||||||
model: config.model,
|
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
|
function extractText( content: unknown ): string
|
||||||
{
|
{
|
||||||
if ( typeof content === "string" ) return content;
|
if ( typeof content === "string" ) return content;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Router } from 'express';
|
||||||
import { requireAuth } from '../../auth-connector/source/server/auth';
|
import { requireAuth } from '../../auth-connector/source/server/auth';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
router.use( requireAuth );
|
router.use( requireAuth );
|
||||||
|
|
@ -114,4 +115,125 @@ router.post( '/delete', ( req, res ) =>
|
||||||
res.json( { ok: true } );
|
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;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -192,12 +192,13 @@ router.post( "/:projectId/create", ( req, res ) =>
|
||||||
|
|
||||||
router.post( "/chat", async ( 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;
|
id: string;
|
||||||
message: string;
|
message: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
rojoPath?: string;
|
rojoPath?: string;
|
||||||
|
localRoot?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
if ( !id || !message )
|
if ( !id || !message )
|
||||||
|
|
@ -214,9 +215,23 @@ router.post( "/chat", async ( req, res ) =>
|
||||||
tools: projectId ? RojoToolRegistry.definitions() : [],
|
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 )
|
if ( content )
|
||||||
{
|
{
|
||||||
|
|
@ -227,7 +242,17 @@ router.post( "/chat", async ( req, res ) =>
|
||||||
|
|
||||||
config.systemPrompt = settings.systemPrompt ?? "";
|
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";
|
const tunnelServer = process.env.TUNNEL_SERVER_URL || "https://tunnel.rokojori.com";
|
||||||
config.baseURL = `${ tunnelServer }/t/${ ep.tunnelId }/v1`;
|
config.baseURL = `${ tunnelServer }/t/${ ep.tunnelId }/v1`;
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ var NAV_DATA = {
|
||||||
title: 'History',
|
title: 'History',
|
||||||
path: 'history/index.html',
|
path: 'history/index.html',
|
||||||
children: [
|
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: '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: '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' },
|
{ 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>
|
<p class="subtitle">Log of all working sessions, most recent first.</p>
|
||||||
</header>
|
</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>
|
<section>
|
||||||
<h2>2026 — July</h2>
|
<h2>2026 — July</h2>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue