@z_ptah/agent 0.0.19 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +9 -7
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import"dotenv/config";import{execSync as V}from"child_process";function k(e){let o=process.env[e];if(!o)throw new Error(`Missing required environment variable: ${e}`);return o}function F(){if(process.env.CLAUDE_PATH)return process.env.CLAUDE_PATH;try{return V("which claude",{encoding:"utf-8"}).trim()}catch{return"/usr/bin/claude"}}var s={serverUrl:k("PTAH_SERVER_URL"),token:k("PTAH_AGENT_TOKEN"),healthPort:Number(process.env.AGENT_HEALTH_PORT||9090),heartbeatIntervalMs:Number(process.env.HEARTBEAT_INTERVAL_MS||3e4),reconnectIntervalMs:Number(process.env.RECONNECT_INTERVAL_MS||5e3),projectsDir:process.env.PROJECTS_DIR||"/projects",claudePath:F()};import $ from"ws";import{spawn as W}from"child_process";import{mkdirSync as q}from"fs";function D(e){let{commandId:o,sessionId:t,projectDir:r,message:n,claudeSessionId:a}=e,d=["-p",n,"--output-format","stream-json","--verbose","--allowedTools","Bash,Read,Edit,Write,Glob,Grep","--append-system-prompt","Your text responses are rendered as markdown in a web UI. Use markdown formatting (tables, code blocks, lists, headings) for readability. You may freely create/edit files as needed \u2014 but always include a summary of what you did in your text response so the user can see it in the UI."];a&&d.push("--resume",a),q(r,{recursive:!0}),console.log(`Starting Claude for session ${t} in ${r}`);let u=W(s.claudePath,d,{cwd:r,env:{...process.env},stdio:["pipe","pipe","pipe"]}),g="",E;function T(i){if(i.trim())try{let p=JSON.parse(i);if(p.session_id&&(E=p.session_id),p.type==="assistant"&&p.message?.content)for(let f of p.message.content)f.type==="text"&&f.text&&l({type:"agent:claude-output",payload:{commandId:o,sessionId:t,output:f.text,isComplete:!1}})}catch{}}u.stdout.on("data",i=>{g+=i.toString();let p=g.split(`
3
- `);g=p.pop()||"";for(let f of p)T(f)}),u.stderr.on("data",i=>{}),u.on("close",i=>{g.trim()&&T(g),console.log(`Claude exited with code ${i} for session ${t}`),w(t),l({type:"agent:claude-output",payload:{commandId:o,sessionId:t,output:i===0?"":`Process exited with code ${i}`,isComplete:!0,claudeSessionId:E??a}})}),u.on("error",i=>{console.error(`Claude spawn error for session ${t}:`,i),w(t),l({type:"agent:claude-output",payload:{commandId:o,sessionId:t,output:`Error spawning Claude: ${i.message}`,isComplete:!0}})})}var x=new Map;function N(e){if(x.has(e.sessionId)){console.warn(`Session ${e.sessionId} already has an active Claude process`);return}x.set(e.sessionId,{commandId:e.commandId}),D(e)}function w(e){x.delete(e)}import{spawn as X}from"child_process";async function R(e){return new Promise((o,t)=>{console.log(`Building Docker in ${e}...`);let r=X("docker",["compose","up","-d","--build"],{cwd:e,stdio:"inherit"});r.on("close",n=>{n===0?(console.log("Docker build successful"),o()):t(new Error(`Docker build failed with code ${n}`))}),r.on("error",n=>{t(new Error(`Docker build error: ${n.message}`))})})}import{writeFile as z}from"fs/promises";import{spawn as Y}from"child_process";function y(e,o){return new Promise((t,r)=>{let n=Y(e,o,{stdio:"inherit"});n.on("close",a=>{a===0?t():r(new Error(`${e} failed with code ${a}`))}),n.on("error",r)})}async function O(e,o,t,r){let n=/^[a-z0-9][a-z0-9-]*$/;if(!n.test(e)||!n.test(o))throw new Error("Invalid project or org code for Nginx config");if(t<1024||t>65535||r<1024||r>65535)throw new Error("Invalid port number");let a=`${e}.${o}.mybptah.com`,d=`
2
+ import"dotenv/config";import{execSync as ee}from"child_process";function U(e){let t=process.env[e];if(!t)throw new Error(`Missing required environment variable: ${e}`);return t}function te(){if(process.env.CLAUDE_PATH)return process.env.CLAUDE_PATH;try{return ee("which claude",{encoding:"utf-8"}).trim()}catch{return"/usr/bin/claude"}}var i={serverUrl:U("PTAH_SERVER_URL"),token:U("PTAH_AGENT_TOKEN"),healthPort:Number(process.env.AGENT_HEALTH_PORT||9090),heartbeatIntervalMs:Number(process.env.HEARTBEAT_INTERVAL_MS||3e4),reconnectIntervalMs:Number(process.env.RECONNECT_INTERVAL_MS||5e3),projectsDir:process.env.PROJECTS_DIR||"/projects",claudePath:te()};import A from"ws";import{spawn as le}from"child_process";import{mkdirSync as de}from"fs";import{randomUUID as $}from"crypto";import{randomUUID as y}from"crypto";function H(e,t,o){let r=e.trim();if(!r)return[];let s;try{s=JSON.parse(r)}catch{return[]}let n=[],c=new Date().toISOString();if(s.type==="content_block_delta"){let a=s.delta;return a?.type==="text_delta"&&a.text&&n.push({kind:"text_delta",id:y(),sessionId:t,commandId:o,timestamp:c,role:"assistant",content:a.text}),a?.type==="thinking_delta"&&a.thinking&&n.push({kind:"thinking",id:y(),sessionId:t,commandId:o,timestamp:c,role:"assistant",content:a.thinking}),n}if(s.type==="content_block_start"){let a=s.content_block;return a?.type==="tool_use"&&n.push({kind:"tool_use",id:y(),sessionId:t,commandId:o,timestamp:c,role:"assistant",toolName:a.name||"tool",toolId:a.id||void 0,toolInput:{}}),n}if(s.type==="content_block_stop"||s.type==="message_start"||s.type==="message_delta"||s.type==="message_stop")return n;if(s.type==="assistant"&&s.message){let a=s.message;if(Array.isArray(a.content))for(let u of a.content){let h=oe(u.type);if(!h)continue;let m={kind:h,id:y(),sessionId:t,commandId:o,timestamp:c,role:"assistant"};switch(h){case"text":m.content=u.text||"";break;case"tool_use":m.toolName=u.name||"tool",m.toolId=u.id||void 0,m.toolInput=u.input??{};break;case"thinking":m.content=u.thinking||"";break}n.push(m)}}return s.type==="tool_result"&&n.push({kind:"tool_result",id:y(),sessionId:t,commandId:o,timestamp:c,role:"assistant",toolId:s.tool_use_id||void 0,content:typeof s.content=="string"?s.content:JSON.stringify(s.content??""),isError:s.is_error||!1}),s.type==="result"&&n.push({kind:"complete",id:y(),sessionId:t,commandId:o,timestamp:c,role:"assistant",content:s.result||"",metadata:{sessionId:s.session_id,cost:s.cost_usd??s.cost,totalCost:s.total_cost_usd,duration:s.duration_ms,numTurns:s.num_turns}}),n}function oe(e){switch(e){case"text":return"text";case"tool_use":return"tool_use";case"thinking":return"thinking";default:return null}}import{mkdirSync as se,appendFileSync as re,existsSync as ne,readFileSync as ae}from"fs";import{join as ie,dirname as ce}from"path";function z(e,t){return ie(i.projectsDir,e,"chat-history",`${t}.jsonl`)}function _(e,t){let o=z(e,t.sessionId);se(ce(o),{recursive:!0}),re(o,JSON.stringify(t)+`
3
+ `,"utf-8")}function L(e,t){let o=z(e,t);if(!ne(o))return[];let r=ae(o,"utf-8"),s=[];for(let n of r.split(`
4
+ `))if(n.trim())try{s.push(JSON.parse(n))}catch{}return s}function J(e){let{commandId:t,sessionId:o,projectId:r,projectDir:s,message:n,claudeSessionId:c,systemPrompt:a}=e,h=["-p",n,"--output-format","stream-json","--verbose","--allowedTools","Bash,Read,Edit,Write,Glob,Grep","--append-system-prompt",a||"Your text responses are rendered as markdown in a web UI. Use markdown formatting (tables, code blocks, lists, headings) for readability. You may freely create/edit files as needed \u2014 but always include a summary of what you did in your text response so the user can see it in the UI."];c&&h.push("--resume",c),de(s,{recursive:!0}),console.log(`Starting Claude for session ${o} in ${s}`);let m={kind:"text",id:$(),sessionId:o,commandId:t,timestamp:new Date().toISOString(),role:"user",content:n};_(r,m),d({type:"agent:claude-message",payload:m});let I=le(i.claudePath,h,{cwd:s,env:{...process.env},stdio:["pipe","pipe","pipe"]}),S="",Z;function j(l){if(!l.trim())return;try{let f=JSON.parse(l);f.session_id&&(Z=f.session_id)}catch{return}let g=H(l,o,t);for(let f of g)f.kind!=="text_delta"&&_(r,f),d({type:"agent:claude-message",payload:f})}I.stdout.on("data",l=>{S+=l.toString();let g=S.split(`
5
+ `);S=g.pop()||"";for(let f of g)j(f)}),I.stderr.on("data",l=>{}),I.on("close",l=>{if(S.trim()&&j(S),console.log(`Claude exited with code ${l} for session ${o}`),P(o),l!==0){let g={kind:"error",id:$(),sessionId:o,commandId:t,timestamp:new Date().toISOString(),role:"assistant",content:`Process exited with code ${l}`};_(r,g),d({type:"agent:claude-message",payload:g})}}),I.on("error",l=>{console.error(`Claude spawn error for session ${o}:`,l),P(o);let g={kind:"error",id:$(),sessionId:o,commandId:t,timestamp:new Date().toISOString(),role:"assistant",content:`Error spawning Claude: ${l.message}`};_(r,g),d({type:"agent:claude-message",payload:g})})}var M=new Map;function B(e){if(M.has(e.sessionId)){console.warn(`Session ${e.sessionId} already has an active Claude process`);return}M.set(e.sessionId,{commandId:e.commandId}),J(e)}function P(e){M.delete(e)}import{spawn as pe}from"child_process";async function F(e){return new Promise((t,o)=>{console.log(`Building Docker in ${e}...`);let r=pe("docker",["compose","up","-d","--build"],{cwd:e,stdio:"inherit"});r.on("close",s=>{s===0?(console.log("Docker build successful"),t()):o(new Error(`Docker build failed with code ${s}`))}),r.on("error",s=>{o(new Error(`Docker build error: ${s.message}`))})})}import{writeFile as ge}from"fs/promises";import{spawn as ue}from"child_process";function x(e,t){return new Promise((o,r)=>{let s=ue(e,t,{stdio:"inherit"});s.on("close",n=>{n===0?o():r(new Error(`${e} failed with code ${n}`))}),s.on("error",r)})}async function G(e,t,o,r){let s=/^[a-z0-9][a-z0-9-]*$/;if(!s.test(e)||!s.test(t))throw new Error("Invalid project or org code for Nginx config");if(o<1024||o>65535||r<1024||r>65535)throw new Error("Invalid port number");let n=`${e}.${t}.mybptah.com`,c=`
4
6
  server {
5
7
  listen 80;
6
- server_name ${a};
8
+ server_name ${n};
7
9
 
8
10
  location /api {
9
- proxy_pass http://127.0.0.1:${t};
11
+ proxy_pass http://127.0.0.1:${o};
10
12
  proxy_http_version 1.1;
11
13
  proxy_set_header Upgrade $http_upgrade;
12
14
  proxy_set_header Connection 'upgrade';
@@ -25,6 +27,6 @@ server {
25
27
  proxy_cache_bypass $http_upgrade;
26
28
  }
27
29
  }
28
- `,u=`/etc/nginx/sites-available/${a}`,g=`/etc/nginx/sites-enabled/${a}`;await z(u,d);try{await y("ln",["-sf",u,g])}catch{}return await y("nginx",["-t"]),await y("nginx",["-s","reload"]),console.log(`Nginx configured for ${a}`),a}async function U(e){try{await y("certbot",["--nginx","-d",e,"--non-interactive","--agree-tos","--email","ssl@mybptah.com"]),console.log(`SSL configured for ${e}`)}catch(o){console.error(`SSL setup failed for ${e}:`,o)}}function v(e,o,t){let r={type:"agent:deploy-status",payload:{commandId:e.commandId,deploymentId:e.deploymentId,status:o,...t}};l(r)}async function j(e){try{v(e,"building"),await R(e.projectDir),v(e,"deploying");let r=await O(e.projectCode,e.orgCode,3001,3002);await U(r).catch(()=>{});let n=`https://${r}`;v(e,"live",{demoUrl:n}),console.log(`Deploy complete: ${n}`)}catch(o){let t=o instanceof Error?o.message:"Unknown deploy error";console.error("Deploy failed:",t),v(e,"failed",{errorMessage:t})}}function H(e){switch(e.type){case"server:auth-result":e.payload.success||(console.error("Authentication failed:",e.payload.error),process.exit(1));break;case"server:command":console.log(`Received command: ${e.payload.action}`,e.payload);break;case"server:doc-sync":console.log(`Doc sync: ${e.payload.action} ${e.payload.fileName}`);break;case"server:claude-start":{let{commandId:o,sessionId:t,projectId:r,message:n,claudeSessionId:a,projectDir:d}=e.payload;console.log(`Starting Claude session ${t} for project ${r}`),N({commandId:o,sessionId:t,projectDir:d,message:n,claudeSessionId:a});break}case"server:claude-stop":{console.log(`Stop requested for session ${e.payload.sessionId}`);break}case"server:deploy-start":{let{commandId:o,deploymentId:t,projectId:r,projectCode:n,orgCode:a,projectDir:d}=e.payload;console.log(`Starting deployment for project ${n}`),j({commandId:o,deploymentId:t,projectId:r,projectCode:n,orgCode:a,projectDir:d});break}default:console.warn("Unknown message type:",e.type)}}import{spawn as K}from"child_process";async function L(){return new Promise(e=>{let o=K(s.claudePath,["auth","status"],{env:{...process.env},stdio:["pipe","pipe","pipe"]}),t="";o.stdout.on("data",r=>{t+=r.toString()}),o.stderr.on("data",r=>{t+=r.toString()}),o.on("close",()=>{try{let r=JSON.parse(t.trim());e({loggedIn:r.loggedIn===!0,email:r.email,subscriptionType:r.subscriptionType})}catch{e({loggedIn:!1})}}),o.on("error",()=>{e({loggedIn:!1})})})}function B(e){b({loggedIn:e.loggedIn,email:e.email}),l({type:"agent:claude-status",payload:e})}var c=null,S=null,m=null,h=!1,I=0,G={loggedIn:!1};function b(e){G=e}var Q=6e4;function l(e){c?.readyState===$.OPEN&&c.send(JSON.stringify(e))}function Z(){_(),S=setInterval(()=>{let e={type:"agent:heartbeat",payload:{cpuUsage:0,memoryUsage:Math.round(process.memoryUsage().heapUsed/1024/1024),activeProjects:0,claude:G}};l(e)},s.heartbeatIntervalMs)}function _(){S&&(clearInterval(S),S=null)}function ee(){if(m)return;let e=Math.min(s.reconnectIntervalMs*Math.pow(2,I),Q);I++,console.log(`Reconnecting in ${e/1e3}s (attempt ${I})...`),m=setTimeout(()=>{m=null,C()},e)}function C(){h||c?.readyState===$.OPEN||(h=!0,console.log(`Connecting to ${s.serverUrl}...`),c=new $(s.serverUrl),c.on("open",()=>{h=!1,console.log("Connected to Ptah Server");let e={type:"agent:auth",payload:{token:s.token}};l(e)}),c.on("message",e=>{try{let o=JSON.parse(e.toString());H(o),o.type==="server:auth-result"&&o.payload?.success&&(console.log(`Authenticated as VPS ${o.payload.vpsId}`),I=0,Z(),L().then(t=>{b({loggedIn:t.loggedIn,email:t.email}),B(t)}))}catch(o){console.error("Failed to parse message:",o)}}),c.on("close",()=>{h=!1,_(),console.log("Disconnected from Ptah Server"),ee()}),c.on("error",e=>{h=!1,console.error("WebSocket error:",e.message)}))}function P(){m&&(clearTimeout(m),m=null),_(),c&&(c.close(),c=null)}import oe from"fastify";var A=oe({logger:!1});A.get("/health",async()=>({status:"ok",timestamp:new Date().toISOString(),uptime:process.uptime()}));async function J(){await A.listen({port:s.healthPort,host:"0.0.0.0"}),console.log(`Agent health server on http://0.0.0.0:${s.healthPort}`)}async function M(){await A.close()}async function te(){console.log("Starting Ptah Agent..."),console.log(` Server: ${s.serverUrl}`),console.log(` Health port: ${s.healthPort}`),console.log(` Projects dir: ${s.projectsDir}`),await J(),C()}process.on("SIGINT",async()=>{console.log(`
29
- Shutting down...`),P(),await M(),process.exit(0)});process.on("SIGTERM",async()=>{console.log(`
30
- Shutting down...`),P(),await M(),process.exit(0)});te().catch(e=>{console.error("Failed to start agent:",e),process.exit(1)});
30
+ `,a=`/etc/nginx/sites-available/${n}`,u=`/etc/nginx/sites-enabled/${n}`;await ge(a,c);try{await x("ln",["-sf",a,u])}catch{}return await x("nginx",["-t"]),await x("nginx",["-s","reload"]),console.log(`Nginx configured for ${n}`),n}async function q(e){try{await x("certbot",["--nginx","-d",e,"--non-interactive","--agree-tos","--email","ssl@mybptah.com"]),console.log(`SSL configured for ${e}`)}catch(t){console.error(`SSL setup failed for ${e}:`,t)}}function b(e,t,o){let r={type:"agent:deploy-status",payload:{commandId:e.commandId,deploymentId:e.deploymentId,status:t,...o}};d(r)}async function V(e){try{b(e,"building"),await F(e.projectDir),b(e,"deploying");let r=await G(e.projectCode,e.orgCode,3001,3002);await q(r).catch(()=>{});let s=`https://${r}`;b(e,"live",{demoUrl:s}),console.log(`Deploy complete: ${s}`)}catch(t){let o=t instanceof Error?t.message:"Unknown deploy error";console.error("Deploy failed:",o),b(e,"failed",{errorMessage:o})}}function K(e){switch(e.type){case"server:auth-result":e.payload.success||(console.error("Authentication failed:",e.payload.error),process.exit(1));break;case"server:command":console.log(`Received command: ${e.payload.action}`,e.payload);break;case"server:doc-sync":console.log(`Doc sync: ${e.payload.action} ${e.payload.fileName}`);break;case"server:claude-start":{let{commandId:t,sessionId:o,projectId:r,message:s,claudeSessionId:n,projectDir:c,systemPrompt:a}=e.payload;console.log(`Starting Claude session ${o} for project ${r}`),B({commandId:t,sessionId:o,projectId:r,projectDir:c,message:s,claudeSessionId:n,systemPrompt:a});break}case"server:claude-stop":{console.log(`Stop requested for session ${e.payload.sessionId}`);break}case"server:chat-history-request":{let{sessionId:t,projectId:o}=e.payload;console.log(`Chat history request for session ${t}`);let r=L(o,t);d({type:"agent:chat-history",payload:{sessionId:t,messages:r}});break}case"server:deploy-start":{let{commandId:t,deploymentId:o,projectId:r,projectCode:s,orgCode:n,projectDir:c}=e.payload;console.log(`Starting deployment for project ${s}`),V({commandId:t,deploymentId:o,projectId:r,projectCode:s,orgCode:n,projectDir:c});break}default:console.warn("Unknown message type:",e.type)}}import{spawn as me}from"child_process";async function W(){return new Promise(e=>{let t=me(i.claudePath,["auth","status"],{env:{...process.env},stdio:["pipe","pipe","pipe"]}),o="";t.stdout.on("data",r=>{o+=r.toString()}),t.stderr.on("data",r=>{o+=r.toString()}),t.on("close",()=>{try{let r=JSON.parse(o.trim());e({loggedIn:r.loggedIn===!0,email:r.email,subscriptionType:r.subscriptionType})}catch{e({loggedIn:!1})}}),t.on("error",()=>{e({loggedIn:!1})})})}function X(e){N({loggedIn:e.loggedIn,email:e.email}),d({type:"agent:claude-status",payload:e})}var p=null,k=null,v=null,w=!1,C=0,Y={loggedIn:!1};function N(e){Y=e}var fe=6e4;function d(e){p?.readyState===A.OPEN&&p.send(JSON.stringify(e))}function he(){E(),k=setInterval(()=>{let e={type:"agent:heartbeat",payload:{cpuUsage:0,memoryUsage:Math.round(process.memoryUsage().heapUsed/1024/1024),activeProjects:0,claude:Y}};d(e)},i.heartbeatIntervalMs)}function E(){k&&(clearInterval(k),k=null)}function ye(){if(v)return;let e=Math.min(i.reconnectIntervalMs*Math.pow(2,C),fe);C++,console.log(`Reconnecting in ${e/1e3}s (attempt ${C})...`),v=setTimeout(()=>{v=null,D()},e)}function D(){w||p?.readyState===A.OPEN||(w=!0,console.log(`Connecting to ${i.serverUrl}...`),p=new A(i.serverUrl),p.on("open",()=>{w=!1,console.log("Connected to Ptah Server");let e={type:"agent:auth",payload:{token:i.token}};d(e)}),p.on("message",e=>{try{let t=JSON.parse(e.toString());K(t),t.type==="server:auth-result"&&t.payload?.success&&(console.log(`Authenticated as VPS ${t.payload.vpsId}`),C=0,he(),W().then(o=>{N({loggedIn:o.loggedIn,email:o.email}),X(o)}))}catch(t){console.error("Failed to parse message:",t)}}),p.on("close",()=>{w=!1,E(),console.log("Disconnected from Ptah Server"),ye()}),p.on("error",e=>{w=!1,console.error("WebSocket error:",e.message)}))}function T(){v&&(clearTimeout(v),v=null),E(),p&&(p.close(),p=null)}import ve from"fastify";var R=ve({logger:!1});R.get("/health",async()=>({status:"ok",timestamp:new Date().toISOString(),uptime:process.uptime()}));async function Q(){await R.listen({port:i.healthPort,host:"0.0.0.0"}),console.log(`Agent health server on http://0.0.0.0:${i.healthPort}`)}async function O(){await R.close()}async function Se(){console.log("Starting Ptah Agent..."),console.log(` Server: ${i.serverUrl}`),console.log(` Health port: ${i.healthPort}`),console.log(` Projects dir: ${i.projectsDir}`),await Q(),D()}process.on("SIGINT",async()=>{console.log(`
31
+ Shutting down...`),T(),await O(),process.exit(0)});process.on("SIGTERM",async()=>{console.log(`
32
+ Shutting down...`),T(),await O(),process.exit(0)});Se().catch(e=>{console.error("Failed to start agent:",e),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@z_ptah/agent",
3
- "version": "0.0.19",
3
+ "version": "0.0.21",
4
4
  "type": "module",
5
5
  "description": "Ptah VPS Agent — connects to Ptah Cloud, manages Claude sessions and deployments",
6
6
  "bin": {