@z_ptah/agent 0.0.27 → 0.0.28

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 +8 -8
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import"dotenv/config";import{execSync as fe}from"child_process";function F(e){let t=process.env[e];if(!t)throw new Error(`Missing required environment variable: ${e}`);return t}function ye(){if(process.env.CLAUDE_PATH)return process.env.CLAUDE_PATH;try{return fe("which claude",{encoding:"utf-8"}).trim()}catch{return"/usr/bin/claude"}}var c={serverUrl:F("PTAH_SERVER_URL"),token:F("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:ye()};import R from"ws";import{spawn as Ue}from"child_process";import{mkdirSync as je}from"fs";import{randomUUID as P}from"crypto";import{randomUUID as v}from"crypto";function J(e,t,o){let r=e.trim();if(!r)return[];let n;try{n=JSON.parse(r)}catch{return[]}let s=[],i=new Date().toISOString();if(n.type==="content_block_delta"){let a=n.delta;return a?.type==="text_delta"&&a.text&&s.push({kind:"text_delta",id:v(),sessionId:t,commandId:o,timestamp:i,role:"assistant",content:a.text}),a?.type==="thinking_delta"&&a.thinking&&s.push({kind:"thinking",id:v(),sessionId:t,commandId:o,timestamp:i,role:"assistant",content:a.thinking}),s}if(n.type==="content_block_start"){let a=n.content_block;return a?.type==="tool_use"&&s.push({kind:"tool_use",id:v(),sessionId:t,commandId:o,timestamp:i,role:"assistant",toolName:a.name||"tool",toolId:a.id||void 0,toolInput:{}}),s}if(n.type==="content_block_stop"||n.type==="message_start"||n.type==="message_delta"||n.type==="message_stop")return s;if(n.type==="assistant"&&n.message){let a=n.message;if(Array.isArray(a.content))for(let d of a.content){let f=he(d.type);if(!f)continue;let g={kind:f,id:v(),sessionId:t,commandId:o,timestamp:i,role:"assistant"};switch(f){case"text":g.content=d.text||"";break;case"tool_use":g.toolName=d.name||"tool",g.toolId=d.id||void 0,g.toolInput=d.input??{};break;case"thinking":g.content=d.thinking||"";break}s.push(g)}}return n.type==="tool_result"&&s.push({kind:"tool_result",id:v(),sessionId:t,commandId:o,timestamp:i,role:"assistant",toolId:n.tool_use_id||void 0,content:typeof n.content=="string"?n.content:JSON.stringify(n.content??""),isError:n.is_error||!1}),n.type==="result"&&s.push({kind:"complete",id:v(),sessionId:t,commandId:o,timestamp:i,role:"assistant",content:n.result||"",metadata:{sessionId:n.session_id,cost:n.cost_usd??n.cost,totalCost:n.total_cost_usd,duration:n.duration_ms,numTurns:n.num_turns}}),s}function he(e){switch(e){case"text":return"text";case"tool_use":return"tool_use";case"thinking":return"thinking";default:return null}}import{mkdirSync as ve,appendFileSync as xe,existsSync as Se,readFileSync as we}from"fs";import{join as be,dirname as ke}from"path";function q(e,t){return be(c.projectsDir,e,"chat-history",`${t}.jsonl`)}function w(e,t){let o=q(e,t.sessionId);ve(ke(o),{recursive:!0}),xe(o,JSON.stringify(t)+`
3
- `,"utf-8")}function B(e,t){let o=q(e,t);if(!Se(o))return[];let r=we(o,"utf-8"),n=[];for(let s of r.split(`
4
- `))if(s.trim())try{n.push(JSON.parse(s))}catch{}return n}import{readdirSync as Pe,statSync as Ae,readFileSync as Ne,writeFileSync as De,existsSync as Ee}from"fs";import{join as M}from"path";import{readFileSync as _e,existsSync as Ie}from"fs";import{basename as $e}from"path";var Ce={".xlsx":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",".xls":"application/vnd.ms-excel",".pdf":"application/pdf",".docx":"application/vnd.openxmlformats-officedocument.wordprocessingml.document",".doc":"application/msword",".csv":"text/csv",".json":"application/json",".md":"text/markdown",".txt":"text/plain",".zip":"application/zip"};function Me(e){let t=e.substring(e.lastIndexOf(".")).toLowerCase();return Ce[t]||"application/octet-stream"}async function G(e,t){if(!Ie(t))return console.warn(`[uploader] File not found: ${t}`),!1;let o=$e(t),r=Me(o),n=_e(t),s=c.serverUrl.replace(/^wss:/,"https:").replace(/^ws:/,"http:").replace(/\/ws\/agent$/,""),i=new FormData;i.append("projectId",e),i.append("file",new Blob([n],{type:r}),o);try{let a=await fetch(`${s}/agent/documents/upload`,{method:"POST",headers:{Authorization:`Bearer ${c.token}`},body:i});if(!a.ok){let d=await a.text();return console.error(`[uploader] Upload failed (${a.status}): ${d}`),!1}return console.log(`[uploader] Uploaded ${o} for project ${e}`),!0}catch(a){return console.error("[uploader] Upload error:",a),!1}}var Te=new Set([".xlsx",".xls",".csv",".pdf",".docx",".doc"]),V=".ptah-uploaded.json";function Re(e){let t=M(e,V);if(!Ee(t))return{};try{return JSON.parse(Ne(t,"utf-8"))}catch{return{}}}function Oe(e,t){De(M(e,V),JSON.stringify(t,null,2))}async function K(e,t){let o=Re(t),r=!1,n;try{n=Pe(t)}catch{return}for(let s of n){let i=s.substring(s.lastIndexOf(".")).toLowerCase();if(!Te.has(i))continue;let a=M(t,s);try{let d=Ae(a);if(!d.isFile())continue;let f=o[s];if(f&&f>=d.mtimeMs)continue;console.log(`[auto-upload] Uploading ${s}...`),await G(e,a)&&(o[s]=d.mtimeMs,r=!0)}catch(d){console.error(`[auto-upload] Failed to process ${s}:`,d)}}r&&Oe(t,o)}function W(e){let{commandId:t,sessionId:o,projectId:r,projectDir:n,message:s,claudeSessionId:i,systemPrompt:a}=e,f=["-p",s,"--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."];i&&f.push("--resume",i),je(n,{recursive:!0}),console.log(`Starting Claude for session ${o} in ${n}`);let g={kind:"text",id:P(),sessionId:o,commandId:t,timestamp:new Date().toISOString(),role:"user",content:s};w(r,g),l({type:"agent:claude-message",payload:g});let k=Ue(c.claudePath,f,{cwd:n,env:{...process.env},stdio:["pipe","pipe","pipe"]}),S="",ge;function L(p){if(!p.trim())return;try{let y=JSON.parse(p);y.session_id&&(ge=y.session_id)}catch{return}let u=J(p,o,t);for(let y of u)y.kind!=="text_delta"&&w(r,y),l({type:"agent:claude-message",payload:y})}k.stdout.on("data",p=>{S+=p.toString();let u=S.split(`
5
- `);S=u.pop()||"";for(let y of u)L(y)}),k.stderr.on("data",p=>{}),k.on("close",p=>{if(S.trim()&&L(S),console.log(`Claude exited with code ${p} for session ${o}`),A(o),p===0&&K(r,n).catch(u=>console.error("[auto-upload] Error:",u)),p!==0){let u={kind:"error",id:P(),sessionId:o,commandId:t,timestamp:new Date().toISOString(),role:"assistant",content:`Process exited with code ${p}`};w(r,u),l({type:"agent:claude-message",payload:u})}}),k.on("error",p=>{console.error(`Claude spawn error for session ${o}:`,p),A(o);let u={kind:"error",id:P(),sessionId:o,commandId:t,timestamp:new Date().toISOString(),role:"assistant",content:`Error spawning Claude: ${p.message}`};w(r,u),l({type:"agent:claude-message",payload:u})})}var N=new Map;function X(e){if(N.has(e.sessionId)){console.warn(`Session ${e.sessionId} already has an active Claude process`);return}N.set(e.sessionId,{commandId:e.commandId}),W(e)}function A(e){N.delete(e)}import{spawn as ze}from"child_process";async function Y(e){return new Promise((t,o)=>{console.log(`Building Docker in ${e}...`);let r=ze("docker",["compose","up","-d","--build"],{cwd:e,stdio:"inherit"});r.on("close",n=>{n===0?(console.log("Docker build successful"),t()):o(new Error(`Docker build failed with code ${n}`))}),r.on("error",n=>{o(new Error(`Docker build error: ${n.message}`))})})}import{writeFile as He}from"fs/promises";import{spawn as Le}from"child_process";function _(e,t){return new Promise((o,r)=>{let n=Le(e,t,{stdio:"inherit"});n.on("close",s=>{s===0?o():r(new Error(`${e} failed with code ${s}`))}),n.on("error",r)})}async function Q(e,t,o,r){let n=/^[a-z0-9][a-z0-9-]*$/;if(!n.test(e)||!n.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 s=`${e}.${t}.mybptah.com`,i=`
2
+ import"dotenv/config";import{execSync as ye}from"child_process";function J(e){let t=process.env[e];if(!t)throw new Error(`Missing required environment variable: ${e}`);return t}function he(){if(process.env.CLAUDE_PATH)return process.env.CLAUDE_PATH;try{return ye("which claude",{encoding:"utf-8"}).trim()}catch{return"/usr/bin/claude"}}var c={serverUrl:J("PTAH_SERVER_URL"),token:J("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:he()};import O from"ws";import{spawn as je}from"child_process";import{mkdirSync as ze}from"fs";import{randomUUID as T}from"crypto";import{randomUUID as v}from"crypto";function q(e,t,o){let r=e.trim();if(!r)return[];let n;try{n=JSON.parse(r)}catch{return[]}let s=[],i=new Date().toISOString();if(n.type==="content_block_delta"){let a=n.delta;return a?.type==="text_delta"&&a.text&&s.push({kind:"text_delta",id:v(),sessionId:t,commandId:o,timestamp:i,role:"assistant",content:a.text}),a?.type==="thinking_delta"&&a.thinking&&s.push({kind:"thinking",id:v(),sessionId:t,commandId:o,timestamp:i,role:"assistant",content:a.thinking}),s}if(n.type==="content_block_start"){let a=n.content_block;return a?.type==="tool_use"&&s.push({kind:"tool_use",id:v(),sessionId:t,commandId:o,timestamp:i,role:"assistant",toolName:a.name||"tool",toolId:a.id||void 0,toolInput:{}}),s}if(n.type==="content_block_stop"||n.type==="message_start"||n.type==="message_delta"||n.type==="message_stop")return s;if(n.type==="assistant"&&n.message){let a=n.message;if(Array.isArray(a.content))for(let l of a.content){let f=ve(l.type);if(!f)continue;let g={kind:f,id:v(),sessionId:t,commandId:o,timestamp:i,role:"assistant"};switch(f){case"text":g.content=l.text||"";break;case"tool_use":g.toolName=l.name||"tool",g.toolId=l.id||void 0,g.toolInput=l.input??{};break;case"thinking":g.content=l.thinking||"";break}s.push(g)}}return n.type==="tool_result"&&s.push({kind:"tool_result",id:v(),sessionId:t,commandId:o,timestamp:i,role:"assistant",toolId:n.tool_use_id||void 0,content:typeof n.content=="string"?n.content:JSON.stringify(n.content??""),isError:n.is_error||!1}),n.type==="result"&&s.push({kind:"complete",id:v(),sessionId:t,commandId:o,timestamp:i,role:"assistant",content:n.result||"",metadata:{sessionId:n.session_id,cost:n.cost_usd??n.cost,totalCost:n.total_cost_usd,duration:n.duration_ms,numTurns:n.num_turns}}),s}function ve(e){switch(e){case"text":return"text";case"tool_use":return"tool_use";case"thinking":return"thinking";default:return null}}import{mkdirSync as xe,appendFileSync as Se,existsSync as we,readFileSync as be}from"fs";import{join as ke,dirname as _e}from"path";function B(e,t){return ke(c.projectsDir,e,"chat-history",`${t}.jsonl`)}function w(e,t){let o=B(e,t.sessionId);xe(_e(o),{recursive:!0}),Se(o,JSON.stringify(t)+`
3
+ `,"utf-8")}function G(e,t){let o=B(e,t);if(!we(o))return[];let r=be(o,"utf-8"),n=[];for(let s of r.split(`
4
+ `))if(s.trim())try{n.push(JSON.parse(s))}catch{}return n}import{readdirSync as Te,statSync as Ae,readFileSync as Ne,writeFileSync as De,existsSync as Ee}from"fs";import{join as P}from"path";import{readFileSync as Ie,existsSync as $e}from"fs";import{basename as Ce}from"path";var Me={".xlsx":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",".xls":"application/vnd.ms-excel",".pdf":"application/pdf",".docx":"application/vnd.openxmlformats-officedocument.wordprocessingml.document",".doc":"application/msword",".csv":"text/csv",".json":"application/json",".md":"text/markdown",".txt":"text/plain",".zip":"application/zip"};function Pe(e){let t=e.substring(e.lastIndexOf(".")).toLowerCase();return Me[t]||"application/octet-stream"}async function V(e,t){if(!$e(t))return console.warn(`[uploader] File not found: ${t}`),!1;let o=Ce(t),r=Pe(o),n=Ie(t),s=c.serverUrl.replace(/^wss:/,"https:").replace(/^ws:/,"http:").replace(/\/ws\/agent$/,""),i=new FormData;i.append("projectId",e),i.append("file",new Blob([n],{type:r}),o);try{let a=await fetch(`${s}/agent/documents/upload`,{method:"POST",headers:{Authorization:`Bearer ${c.token}`},body:i});if(!a.ok){let l=await a.text();return console.error(`[uploader] Upload failed (${a.status}): ${l}`),!1}return console.log(`[uploader] Uploaded ${o} for project ${e}`),!0}catch(a){return console.error("[uploader] Upload error:",a),!1}}var Re=new Set([".xlsx",".xls",".csv",".pdf",".docx",".doc"]),K=".ptah-uploaded.json";function Oe(e){let t=P(e,K);if(!Ee(t))return{};try{return JSON.parse(Ne(t,"utf-8"))}catch{return{}}}function Ue(e,t){De(P(e,K),JSON.stringify(t,null,2))}async function W(e,t){let o=Oe(t),r=!1,n;try{n=Te(t)}catch{return}for(let s of n){let i=s.substring(s.lastIndexOf(".")).toLowerCase();if(!Re.has(i))continue;let a=P(t,s);try{let l=Ae(a);if(!l.isFile())continue;let f=o[s];if(f&&f>=l.mtimeMs)continue;console.log(`[auto-upload] Uploading ${s}...`),await V(e,a)&&(o[s]=l.mtimeMs,r=!0)}catch(l){console.error(`[auto-upload] Failed to process ${s}:`,l)}}r&&Ue(t,o)}function X(e){let{commandId:t,sessionId:o,projectId:r,projectDir:n,message:s,claudeSessionId:i,systemPrompt:a}=e,f=["-p",s,"--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."];i&&f.push("--resume",i),ze(n,{recursive:!0}),console.log(`Starting Claude for session ${o} in ${n}`);let g={kind:"text",id:T(),sessionId:o,commandId:t,timestamp:new Date().toISOString(),role:"user",content:s};w(r,g),d({type:"agent:claude-message",payload:g});let k=je(c.claudePath,f,{cwd:n,env:{...process.env},stdio:["pipe","pipe","pipe"]}),S="",fe;function F(p){if(!p.trim())return;try{let h=JSON.parse(p);h.session_id&&(fe=h.session_id)}catch{return}let u=q(p,o,t);for(let h of u)h.kind!=="text_delta"&&w(r,h),d({type:"agent:claude-message",payload:h})}k.stdout.on("data",p=>{S+=p.toString();let u=S.split(`
5
+ `);S=u.pop()||"";for(let h of u)F(h)}),k.stderr.on("data",p=>{}),k.on("close",p=>{if(S.trim()&&F(S),console.log(`Claude exited with code ${p} for session ${o}`),A(o),p===0&&W(r,n).catch(u=>console.error("[auto-upload] Error:",u)),p!==0){let u={kind:"error",id:T(),sessionId:o,commandId:t,timestamp:new Date().toISOString(),role:"assistant",content:`Process exited with code ${p}`};w(r,u),d({type:"agent:claude-message",payload:u})}}),k.on("error",p=>{console.error(`Claude spawn error for session ${o}:`,p),A(o);let u={kind:"error",id:T(),sessionId:o,commandId:t,timestamp:new Date().toISOString(),role:"assistant",content:`Error spawning Claude: ${p.message}`};w(r,u),d({type:"agent:claude-message",payload:u})})}var N=new Map;function Y(e){if(N.has(e.sessionId)){console.warn(`Session ${e.sessionId} already has an active Claude process`);return}N.set(e.sessionId,{commandId:e.commandId}),X(e)}function A(e){N.delete(e)}import{spawn as He}from"child_process";async function Q(e){return new Promise((t,o)=>{console.log(`Building Docker in ${e}...`);let r=He("docker",["compose","up","-d","--build"],{cwd:e,stdio:"inherit"});r.on("close",n=>{n===0?(console.log("Docker build successful"),t()):o(new Error(`Docker build failed with code ${n}`))}),r.on("error",n=>{o(new Error(`Docker build error: ${n.message}`))})})}import{writeFile as Le}from"fs/promises";import{spawn as Fe}from"child_process";function _(e,t){return new Promise((o,r)=>{let n=Fe(e,t,{stdio:"inherit"});n.on("close",s=>{s===0?o():r(new Error(`${e} failed with code ${s}`))}),n.on("error",r)})}async function Z(e,t,o,r){let n=/^[a-z0-9][a-z0-9-]*$/;if(!n.test(e)||!n.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 s=`${e}.${t}.mybptah.com`,i=`
6
6
  server {
7
7
  listen 80;
8
8
  server_name ${s};
@@ -27,7 +27,7 @@ server {
27
27
  proxy_cache_bypass $http_upgrade;
28
28
  }
29
29
  }
30
- `,a=`/etc/nginx/sites-available/${s}`,d=`/etc/nginx/sites-enabled/${s}`;await He(a,i);try{await _("ln",["-sf",a,d])}catch{}return await _("nginx",["-t"]),await _("nginx",["-s","reload"]),console.log(`Nginx configured for ${s}`),s}async function Z(e){try{await _("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 I(e,t,o){let r={type:"agent:deploy-status",payload:{commandId:e.commandId,deploymentId:e.deploymentId,status:t,...o}};l(r)}async function ee(e){try{I(e,"building"),await Y(e.projectDir),I(e,"deploying");let r=await Q(e.projectCode,e.orgCode,3001,3002);await Z(r).catch(()=>{});let n=`https://${r}`;I(e,"live",{demoUrl:n}),console.log(`Deploy complete: ${n}`)}catch(t){let o=t instanceof Error?t.message:"Unknown deploy error";console.error("Deploy failed:",o),I(e,"failed",{errorMessage:o})}}import{execSync as D}from"child_process";function te(){try{return D(`docker ps -a --format '{"id":"{{.ID}}","name":"{{.Names}}","image":"{{.Image}}","status":"{{.Status}}","state":"{{.State}}","ports":"{{.Ports}}","createdAt":"{{.CreatedAt}}"}'`,{encoding:"utf-8",timeout:1e4}).trim().split(`
31
- `).filter(t=>t.trim()).map(t=>{let o=JSON.parse(t),r=o.name.match(/^ptah-([a-f0-9-]+)/);return{...o,projectId:r?.[1]||void 0}})}catch(e){return console.error("[docker] Failed to list containers:",e),[]}}function oe(e){try{return D(`docker stop ${e}`,{encoding:"utf-8",timeout:3e4}),{success:!0}}catch(t){return{success:!1,error:t.message}}}function re(e){try{return D(`docker rm -f ${e}`,{encoding:"utf-8",timeout:3e4}),{success:!0}}catch(t){return{success:!1,error:t.message}}}var E=null,ne=!1;async function Fe(){if(ne)return E;ne=!0;try{let{createRequire:e}=await import("module");E=e(import.meta.url)("node-pty"),console.log("[terminal] node-pty loaded successfully")}catch(e){console.warn("[terminal] node-pty not available \u2014 terminal feature disabled",e.message)}return E}var h=new Map;async function se(e,t,o){let r=await Fe();if(!r){l({type:"agent:terminal-exit",payload:{terminalId:e,exitCode:1}});return}if(h.has(e)){console.warn(`[terminal] Terminal ${e} already exists`);return}let n=process.env.SHELL||"/bin/bash",s=r.spawn(n,[],{name:"xterm-256color",cols:t,rows:o,cwd:process.env.HOME||"/",env:process.env});h.set(e,s),s.onData(i=>{l({type:"agent:terminal-output",payload:{terminalId:e,data:i}})}),s.onExit(({exitCode:i})=>{h.delete(e),l({type:"agent:terminal-exit",payload:{terminalId:e,exitCode:i}})}),console.log(`[terminal] Opened ${e} (${t}x${o})`)}function ae(e,t){let o=h.get(e);o&&o.write(t)}function ie(e,t,o){let r=h.get(e);r&&r.resize(t,o)}function ce(e){let t=h.get(e);t&&(t.kill(),h.delete(e),console.log(`[terminal] Closed ${e}`))}function le(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:n,claudeSessionId:s,projectDir:i,systemPrompt:a}=e.payload;console.log(`Starting Claude session ${o} for project ${r}`),X({commandId:t,sessionId:o,projectId:r,projectDir:i,message:n,claudeSessionId:s,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=B(o,t);l({type:"agent:chat-history",payload:{sessionId:t,messages:r}});break}case"server:deploy-start":{let{commandId:t,deploymentId:o,projectId:r,projectCode:n,orgCode:s,projectDir:i}=e.payload;console.log(`Starting deployment for project ${n}`),ee({commandId:t,deploymentId:o,projectId:r,projectCode:n,orgCode:s,projectDir:i});break}case"server:docker-list":{let{requestId:t}=e.payload;console.log(`Docker list request: ${t}`);let o=te();l({type:"agent:docker-list",payload:{requestId:t,containers:o}});break}case"server:docker-stop":{let{requestId:t,containerId:o}=e.payload;console.log(`Docker stop: ${o}`);let r=oe(o);l({type:"agent:docker-action",payload:{requestId:t,...r}});break}case"server:docker-remove":{let{requestId:t,containerId:o}=e.payload;console.log(`Docker remove: ${o}`);let r=re(o);l({type:"agent:docker-action",payload:{requestId:t,...r}});break}case"server:terminal-open":{let{terminalId:t,cols:o,rows:r}=e.payload;se(t,o,r);break}case"server:terminal-input":{ae(e.payload.terminalId,e.payload.data);break}case"server:terminal-resize":{let{terminalId:t,cols:o,rows:r}=e.payload;ie(t,o,r);break}case"server:terminal-close":{ce(e.payload.terminalId);break}default:console.warn("Unknown message type:",e.type)}}import{spawn as Je}from"child_process";async function de(){return new Promise(e=>{let t=Je(c.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 pe(e){T({loggedIn:e.loggedIn,email:e.email}),l({type:"agent:claude-status",payload:e})}var m=null,$=null,x=null,b=!1,C=0,ue={loggedIn:!1};function T(e){ue=e}var qe=6e4;function l(e){m?.readyState===R.OPEN&&m.send(JSON.stringify(e))}function Be(){O(),$=setInterval(()=>{let e={type:"agent:heartbeat",payload:{cpuUsage:0,memoryUsage:Math.round(process.memoryUsage().heapUsed/1024/1024),activeProjects:0,claude:ue}};l(e)},c.heartbeatIntervalMs)}function O(){$&&(clearInterval($),$=null)}function Ge(){if(x)return;let e=Math.min(c.reconnectIntervalMs*Math.pow(2,C),qe);C++,console.log(`Reconnecting in ${e/1e3}s (attempt ${C})...`),x=setTimeout(()=>{x=null,U()},e)}function U(){b||m?.readyState===R.OPEN||(b=!0,console.log(`Connecting to ${c.serverUrl}...`),m=new R(c.serverUrl),m.on("open",()=>{b=!1,console.log("Connected to Ptah Server");let e={type:"agent:auth",payload:{token:c.token}};l(e)}),m.on("message",e=>{try{let t=JSON.parse(e.toString());le(t),t.type==="server:auth-result"&&t.payload?.success&&(console.log(`Authenticated as VPS ${t.payload.vpsId}`),C=0,Be(),de().then(o=>{T({loggedIn:o.loggedIn,email:o.email}),pe(o)}))}catch(t){console.error("Failed to parse message:",t)}}),m.on("close",()=>{b=!1,O(),console.log("Disconnected from Ptah Server"),Ge()}),m.on("error",e=>{b=!1,console.error("WebSocket error:",e.message)}))}function j(){x&&(clearTimeout(x),x=null),O(),m&&(m.close(),m=null)}import Ve from"fastify";var z=Ve({logger:!1});z.get("/health",async()=>({status:"ok",timestamp:new Date().toISOString(),uptime:process.uptime()}));async function me(){await z.listen({port:c.healthPort,host:"0.0.0.0"}),console.log(`Agent health server on http://0.0.0.0:${c.healthPort}`)}async function H(){await z.close()}async function Ke(){console.log("Starting Ptah Agent..."),console.log(` Server: ${c.serverUrl}`),console.log(` Health port: ${c.healthPort}`),console.log(` Projects dir: ${c.projectsDir}`),await me(),U()}process.on("SIGINT",async()=>{console.log(`
32
- Shutting down...`),j(),await H(),process.exit(0)});process.on("SIGTERM",async()=>{console.log(`
33
- Shutting down...`),j(),await H(),process.exit(0)});Ke().catch(e=>{console.error("Failed to start agent:",e),process.exit(1)});
30
+ `,a=`/etc/nginx/sites-available/${s}`,l=`/etc/nginx/sites-enabled/${s}`;await Le(a,i);try{await _("ln",["-sf",a,l])}catch{}return await _("nginx",["-t"]),await _("nginx",["-s","reload"]),console.log(`Nginx configured for ${s}`),s}async function ee(e){try{await _("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 I(e,t,o){let r={type:"agent:deploy-status",payload:{commandId:e.commandId,deploymentId:e.deploymentId,status:t,...o}};d(r)}async function te(e){try{I(e,"building"),await Q(e.projectDir),I(e,"deploying");let r=await Z(e.projectCode,e.orgCode,3001,3002);await ee(r).catch(()=>{});let n=`https://${r}`;I(e,"live",{demoUrl:n}),console.log(`Deploy complete: ${n}`)}catch(t){let o=t instanceof Error?t.message:"Unknown deploy error";console.error("Deploy failed:",o),I(e,"failed",{errorMessage:o})}}import{execSync as D}from"child_process";function oe(){try{return D(`docker ps -a --format '{"id":"{{.ID}}","name":"{{.Names}}","image":"{{.Image}}","status":"{{.Status}}","state":"{{.State}}","ports":"{{.Ports}}","createdAt":"{{.CreatedAt}}"}'`,{encoding:"utf-8",timeout:1e4}).trim().split(`
31
+ `).filter(t=>t.trim()).map(t=>{let o=JSON.parse(t),r=o.name.match(/^ptah-([a-f0-9-]+)/);return{...o,projectId:r?.[1]||void 0}})}catch(e){return console.error("[docker] Failed to list containers:",e),[]}}function re(e){try{return D(`docker stop ${e}`,{encoding:"utf-8",timeout:3e4}),{success:!0}}catch(t){return{success:!1,error:t.message}}}function ne(e){try{return D(`docker rm -f ${e}`,{encoding:"utf-8",timeout:3e4}),{success:!0}}catch(t){return{success:!1,error:t.message}}}var E=null,se=!1;async function Je(){if(se)return E;se=!0;try{let{createRequire:e}=await import("module");E=e(import.meta.url)("node-pty"),console.log("[terminal] node-pty loaded successfully")}catch(e){console.warn("[terminal] node-pty not available \u2014 terminal feature disabled",e.message)}return E}var ae=5*60*1e3,y=new Map;function qe(e){let t=y.get(e);t&&(clearTimeout(t.idleTimer),t.idleTimer=setTimeout(()=>{console.log(`[terminal] ${e} idle timeout \u2014 killing`),$(e)},ae))}async function ie(e,t,o){let r=await Je();if(!r){d({type:"agent:terminal-exit",payload:{terminalId:e,exitCode:1}});return}if(y.has(e)){console.warn(`[terminal] Terminal ${e} already exists`);return}let n=process.env.SHELL||"/bin/bash",s=r.spawn(n,[],{name:"xterm-256color",cols:t,rows:o,cwd:process.env.HOME||"/",env:process.env}),i=setTimeout(()=>{console.log(`[terminal] ${e} idle timeout \u2014 killing`),$(e)},ae);y.set(e,{pty:s,idleTimer:i}),s.onData(a=>{d({type:"agent:terminal-output",payload:{terminalId:e,data:a}})}),s.onExit(({exitCode:a})=>{let l=y.get(e);l&&clearTimeout(l.idleTimer),y.delete(e),d({type:"agent:terminal-exit",payload:{terminalId:e,exitCode:a}})}),console.log(`[terminal] Opened ${e} (${t}x${o})`)}function ce(e,t){let o=y.get(e);o&&(o.pty.write(t),qe(e))}function le(e,t,o){let r=y.get(e);r&&r.pty.resize(t,o)}function $(e){let t=y.get(e);t&&(clearTimeout(t.idleTimer),t.pty.kill(),y.delete(e),console.log(`[terminal] Closed ${e}`))}function de(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:n,claudeSessionId:s,projectDir:i,systemPrompt:a}=e.payload;console.log(`Starting Claude session ${o} for project ${r}`),Y({commandId:t,sessionId:o,projectId:r,projectDir:i,message:n,claudeSessionId:s,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=G(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:n,orgCode:s,projectDir:i}=e.payload;console.log(`Starting deployment for project ${n}`),te({commandId:t,deploymentId:o,projectId:r,projectCode:n,orgCode:s,projectDir:i});break}case"server:docker-list":{let{requestId:t}=e.payload;console.log(`Docker list request: ${t}`);let o=oe();d({type:"agent:docker-list",payload:{requestId:t,containers:o}});break}case"server:docker-stop":{let{requestId:t,containerId:o}=e.payload;console.log(`Docker stop: ${o}`);let r=re(o);d({type:"agent:docker-action",payload:{requestId:t,...r}});break}case"server:docker-remove":{let{requestId:t,containerId:o}=e.payload;console.log(`Docker remove: ${o}`);let r=ne(o);d({type:"agent:docker-action",payload:{requestId:t,...r}});break}case"server:terminal-open":{let{terminalId:t,cols:o,rows:r}=e.payload;ie(t,o,r);break}case"server:terminal-input":{ce(e.payload.terminalId,e.payload.data);break}case"server:terminal-resize":{let{terminalId:t,cols:o,rows:r}=e.payload;le(t,o,r);break}case"server:terminal-close":{$(e.payload.terminalId);break}default:console.warn("Unknown message type:",e.type)}}import{spawn as Be}from"child_process";async function pe(){return new Promise(e=>{let t=Be(c.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 ue(e){R({loggedIn:e.loggedIn,email:e.email}),d({type:"agent:claude-status",payload:e})}var m=null,C=null,x=null,b=!1,M=0,me={loggedIn:!1};function R(e){me=e}var Ge=6e4;function d(e){m?.readyState===O.OPEN&&m.send(JSON.stringify(e))}function Ve(){U(),C=setInterval(()=>{let e={type:"agent:heartbeat",payload:{cpuUsage:0,memoryUsage:Math.round(process.memoryUsage().heapUsed/1024/1024),activeProjects:0,claude:me}};d(e)},c.heartbeatIntervalMs)}function U(){C&&(clearInterval(C),C=null)}function Ke(){if(x)return;let e=Math.min(c.reconnectIntervalMs*Math.pow(2,M),Ge);M++,console.log(`Reconnecting in ${e/1e3}s (attempt ${M})...`),x=setTimeout(()=>{x=null,j()},e)}function j(){b||m?.readyState===O.OPEN||(b=!0,console.log(`Connecting to ${c.serverUrl}...`),m=new O(c.serverUrl),m.on("open",()=>{b=!1,console.log("Connected to Ptah Server");let e={type:"agent:auth",payload:{token:c.token}};d(e)}),m.on("message",e=>{try{let t=JSON.parse(e.toString());de(t),t.type==="server:auth-result"&&t.payload?.success&&(console.log(`Authenticated as VPS ${t.payload.vpsId}`),M=0,Ve(),pe().then(o=>{R({loggedIn:o.loggedIn,email:o.email}),ue(o)}))}catch(t){console.error("Failed to parse message:",t)}}),m.on("close",()=>{b=!1,U(),console.log("Disconnected from Ptah Server"),Ke()}),m.on("error",e=>{b=!1,console.error("WebSocket error:",e.message)}))}function z(){x&&(clearTimeout(x),x=null),U(),m&&(m.close(),m=null)}import We from"fastify";var H=We({logger:!1});H.get("/health",async()=>({status:"ok",timestamp:new Date().toISOString(),uptime:process.uptime()}));async function ge(){await H.listen({port:c.healthPort,host:"0.0.0.0"}),console.log(`Agent health server on http://0.0.0.0:${c.healthPort}`)}async function L(){await H.close()}async function Xe(){console.log("Starting Ptah Agent..."),console.log(` Server: ${c.serverUrl}`),console.log(` Health port: ${c.healthPort}`),console.log(` Projects dir: ${c.projectsDir}`),await ge(),j()}process.on("SIGINT",async()=>{console.log(`
32
+ Shutting down...`),z(),await L(),process.exit(0)});process.on("SIGTERM",async()=>{console.log(`
33
+ Shutting down...`),z(),await L(),process.exit(0)});Xe().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.27",
3
+ "version": "0.0.28",
4
4
  "type": "module",
5
5
  "description": "Ptah VPS Agent — connects to Ptah Cloud, manages Claude sessions and deployments",
6
6
  "bin": {