chainlesschain 0.45.11 → 0.45.19

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 (81) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/AppLayout-B00RARl2.js +1 -0
  3. package/src/assets/web-panel/assets/AppLayout-CFP4dGIJ.css +1 -0
  4. package/src/assets/web-panel/assets/{Chat-5f__rMCR.js → Chat-DXtvKoM0.js} +1 -1
  5. package/src/assets/web-panel/assets/{Cron-C4mrNC4c.js → Cron-BJ4ODHOy.js} +1 -1
  6. package/src/assets/web-panel/assets/Dashboard-3iIpp3zd.js +3 -0
  7. package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
  8. package/src/assets/web-panel/assets/{Logs-CC_Zuh66.js → Logs-CSeKZEG_.js} +1 -1
  9. package/src/assets/web-panel/assets/{McpTools-B15GiN3u.js → McpTools-BYQAK11r.js} +2 -2
  10. package/src/assets/web-panel/assets/{Memory-Dbd7oLOH.js → Memory-gkUAPyuZ.js} +2 -2
  11. package/src/assets/web-panel/assets/{Notes-CEkc49fY.js → Notes-bjNrQgAo.js} +1 -1
  12. package/src/assets/web-panel/assets/{Providers-CjyPHW00.js → Providers-Dbf57Tbv.js} +1 -1
  13. package/src/assets/web-panel/assets/{Services-XFzHMRRd.js → Services-CS0oMdxh.js} +1 -1
  14. package/src/assets/web-panel/assets/{Skills-D8oxmB3U.js → Skills-B2fgruv8.js} +1 -1
  15. package/src/assets/web-panel/assets/Tasks-BJjN_YEm.css +1 -0
  16. package/src/assets/web-panel/assets/Tasks-qULws8pc.js +1 -0
  17. package/src/assets/web-panel/assets/{antd-ChLPLhSn.js → antd-CJSBocer.js} +1 -1
  18. package/src/assets/web-panel/assets/chat-DnH09sSR.js +1 -0
  19. package/src/assets/web-panel/assets/{index-DQ5xXK7O.js → index-CF2CqPYX.js} +2 -2
  20. package/src/assets/web-panel/assets/{markdown-DtbPhnFe.js → markdown-Bo5cVN4u.js} +1 -1
  21. package/src/assets/web-panel/assets/ws-DjelKkD6.js +1 -0
  22. package/src/assets/web-panel/index.html +2 -2
  23. package/src/commands/agent.js +7 -8
  24. package/src/commands/chat.js +9 -11
  25. package/src/commands/serve.js +11 -106
  26. package/src/commands/session.js +185 -18
  27. package/src/commands/ui.js +10 -151
  28. package/src/gateways/repl/agent-repl.js +1 -0
  29. package/src/gateways/repl/chat-repl.js +1 -0
  30. package/src/gateways/ui/web-ui-server.js +1 -0
  31. package/src/gateways/ws/action-protocol.js +83 -0
  32. package/src/gateways/ws/message-dispatcher.js +73 -0
  33. package/src/gateways/ws/session-protocol.js +396 -0
  34. package/src/gateways/ws/task-protocol.js +55 -0
  35. package/src/gateways/ws/worktree-protocol.js +315 -0
  36. package/src/gateways/ws/ws-server.js +4 -0
  37. package/src/gateways/ws/ws-session-gateway.js +1 -0
  38. package/src/harness/background-task-manager.js +506 -0
  39. package/src/harness/background-task-worker.js +48 -0
  40. package/src/harness/compression-telemetry.js +214 -0
  41. package/src/harness/feature-flags.js +157 -0
  42. package/src/harness/jsonl-session-store.js +452 -0
  43. package/src/harness/prompt-compressor.js +416 -0
  44. package/src/harness/worktree-isolator.js +845 -0
  45. package/src/lib/agent-core.js +246 -45
  46. package/src/lib/background-task-manager.js +1 -305
  47. package/src/lib/background-task-worker.js +1 -50
  48. package/src/lib/compression-telemetry.js +5 -0
  49. package/src/lib/feature-flags.js +7 -182
  50. package/src/lib/interaction-adapter.js +32 -6
  51. package/src/lib/jsonl-session-store.js +21 -237
  52. package/src/lib/prompt-compressor.js +10 -351
  53. package/src/lib/sub-agent-context.js +91 -0
  54. package/src/lib/worktree-isolator.js +13 -231
  55. package/src/lib/ws-agent-handler.js +1 -0
  56. package/src/lib/ws-server.js +155 -359
  57. package/src/lib/ws-session-manager.js +82 -1
  58. package/src/repl/agent-repl.js +114 -32
  59. package/src/runtime/agent-runtime.js +417 -0
  60. package/src/runtime/contracts/agent-turn.js +11 -0
  61. package/src/runtime/contracts/session-record.js +31 -0
  62. package/src/runtime/contracts/task-record.js +18 -0
  63. package/src/runtime/contracts/telemetry-record.js +23 -0
  64. package/src/runtime/contracts/worktree-record.js +14 -0
  65. package/src/runtime/index.js +13 -0
  66. package/src/runtime/policies/agent-policy.js +45 -0
  67. package/src/runtime/runtime-context.js +14 -0
  68. package/src/runtime/runtime-events.js +37 -0
  69. package/src/runtime/runtime-factory.js +50 -0
  70. package/src/tools/index.js +22 -0
  71. package/src/tools/legacy-agent-tools.js +171 -0
  72. package/src/tools/registry.js +141 -0
  73. package/src/tools/tool-context.js +28 -0
  74. package/src/tools/tool-permissions.js +28 -0
  75. package/src/tools/tool-telemetry.js +39 -0
  76. package/src/assets/web-panel/assets/AppLayout-19ZC8w11.js +0 -1
  77. package/src/assets/web-panel/assets/AppLayout-CjgO-ML6.css +0 -1
  78. package/src/assets/web-panel/assets/Dashboard-CRFnDUFh.css +0 -1
  79. package/src/assets/web-panel/assets/Dashboard-DsjXpZor.js +0 -3
  80. package/src/assets/web-panel/assets/chat-C_hu-qNs.js +0 -1
  81. package/src/assets/web-panel/assets/ws-DwluTqT5.js +0 -1
@@ -1,4 +1,4 @@
1
- import{g as Yc}from"./antd-ChLPLhSn.js";function Ni(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}let He=Ni();function Mc(a){He=a}const Lc=/[&<>"']/,qc=new RegExp(Lc.source,"g"),xc=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,Hc=new RegExp(xc.source,"g"),Vc={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},$i=a=>Vc[a];function Ne(a,e){if(e){if(Lc.test(a))return a.replace(qc,$i)}else if(xc.test(a))return a.replace(Hc,$i);return a}const zc=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig;function Wc(a){return a.replace(zc,(e,t)=>(t=t.toLowerCase(),t==="colon"?":":t.charAt(0)==="#"?t.charAt(1)==="x"?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const $c=/(^|[^\[])\^/g;function Z(a,e){let t=typeof a=="string"?a:a.source;e=e||"";const n={replace:(r,i)=>{let o=typeof i=="string"?i:i.source;return o=o.replace($c,"$1"),t=t.replace(r,o),n},getRegex:()=>new RegExp(t,e)};return n}function Ki(a){try{a=encodeURI(a).replace(/%25/g,"%")}catch{return null}return a}const pt={exec:()=>null};function Qi(a,e){const t=a.replace(/\|/g,(i,o,s)=>{let l=!1,_=o;for(;--_>=0&&s[_]==="\\";)l=!l;return l?"|":" |"}),n=t.split(/ \|/);let r=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),e)if(n.length>e)n.splice(e);else for(;n.length<e;)n.push("");for(;r<n.length;r++)n[r]=n[r].trim().replace(/\\\|/g,"|");return n}function yt(a,e,t){const n=a.length;if(n===0)return"";let r=0;for(;r<n&&a.charAt(n-r-1)===e;)r++;return a.slice(0,n-r)}function Kc(a,e){if(a.indexOf(e[1])===-1)return-1;let t=0;for(let n=0;n<a.length;n++)if(a[n]==="\\")n++;else if(a[n]===e[0])t++;else if(a[n]===e[1]&&(t--,t<0))return n;return-1}function Xi(a,e,t,n){const r=e.href,i=e.title?Ne(e.title):null,o=a[1].replace(/\\([\[\]])/g,"$1");if(a[0].charAt(0)!=="!"){n.state.inLink=!0;const s={type:"link",raw:t,href:r,title:i,text:o,tokens:n.inlineTokens(o)};return n.state.inLink=!1,s}return{type:"image",raw:t,href:r,title:i,text:Ne(o)}}function Qc(a,e){const t=a.match(/^(\s+)(?:```)/);if(t===null)return e;const n=t[1];return e.split(`
1
+ import{g as Yc}from"./antd-CJSBocer.js";function Ni(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}let He=Ni();function Mc(a){He=a}const Lc=/[&<>"']/,qc=new RegExp(Lc.source,"g"),xc=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,Hc=new RegExp(xc.source,"g"),Vc={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},$i=a=>Vc[a];function Ne(a,e){if(e){if(Lc.test(a))return a.replace(qc,$i)}else if(xc.test(a))return a.replace(Hc,$i);return a}const zc=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig;function Wc(a){return a.replace(zc,(e,t)=>(t=t.toLowerCase(),t==="colon"?":":t.charAt(0)==="#"?t.charAt(1)==="x"?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const $c=/(^|[^\[])\^/g;function Z(a,e){let t=typeof a=="string"?a:a.source;e=e||"";const n={replace:(r,i)=>{let o=typeof i=="string"?i:i.source;return o=o.replace($c,"$1"),t=t.replace(r,o),n},getRegex:()=>new RegExp(t,e)};return n}function Ki(a){try{a=encodeURI(a).replace(/%25/g,"%")}catch{return null}return a}const pt={exec:()=>null};function Qi(a,e){const t=a.replace(/\|/g,(i,o,s)=>{let l=!1,_=o;for(;--_>=0&&s[_]==="\\";)l=!l;return l?"|":" |"}),n=t.split(/ \|/);let r=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),e)if(n.length>e)n.splice(e);else for(;n.length<e;)n.push("");for(;r<n.length;r++)n[r]=n[r].trim().replace(/\\\|/g,"|");return n}function yt(a,e,t){const n=a.length;if(n===0)return"";let r=0;for(;r<n&&a.charAt(n-r-1)===e;)r++;return a.slice(0,n-r)}function Kc(a,e){if(a.indexOf(e[1])===-1)return-1;let t=0;for(let n=0;n<a.length;n++)if(a[n]==="\\")n++;else if(a[n]===e[0])t++;else if(a[n]===e[1]&&(t--,t<0))return n;return-1}function Xi(a,e,t,n){const r=e.href,i=e.title?Ne(e.title):null,o=a[1].replace(/\\([\[\]])/g,"$1");if(a[0].charAt(0)!=="!"){n.state.inLink=!0;const s={type:"link",raw:t,href:r,title:i,text:o,tokens:n.inlineTokens(o)};return n.state.inLink=!1,s}return{type:"image",raw:t,href:r,title:i,text:Ne(o)}}function Qc(a,e){const t=a.match(/^(\s+)(?:```)/);if(t===null)return e;const n=t[1];return e.split(`
2
2
  `).map(r=>{const i=r.match(/^\s+/);if(i===null)return r;const[o]=i;return o.length>=n.length?r.slice(n.length):r}).join(`
3
3
  `)}class Lt{options;rules;lexer;constructor(e){this.options=e||He}space(e){const t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const n=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?n:yt(n,`
4
4
  `)}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const n=t[0],r=Qc(n,t[3]||"");return{type:"code",raw:n,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:r}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let n=t[2].trim();if(/#$/.test(n)){const r=yt(n,"#");(this.options.pedantic||!r||/ $/.test(r))&&(n=r.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:n,tokens:this.lexer.inline(n)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){let n=t[0].replace(/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,`
@@ -0,0 +1 @@
1
+ import{V as H,f as E,c as z}from"./vendor-CN0Iv_qZ.js";let U=0;const p=()=>`wp-${++U}`,C=new Set;function d(e,n={},i={}){return{type:e,kind:i.kind||"server",sessionId:i.sessionId||n.sessionId||null,timestamp:i.timestamp||Date.now(),payload:n}}function B(e){switch(e?.type){case"task:notification":return d("task:notification",{task:e.task},{kind:"server"});case"session-created":return d("session:start",{sessionId:e.sessionId,sessionType:e.sessionType||null,record:e.record||{id:e.sessionId,type:e.sessionType||null,status:"created",history:[],messageCount:0}},{kind:"server",sessionId:e.sessionId});case"session-resumed":return d("session:resume",{sessionId:e.sessionId,history:e.history||[],historyCount:Array.isArray(e.history)?e.history.length:0,record:e.record||{id:e.sessionId,type:null,status:"resumed",history:e.history||[],messageCount:Array.isArray(e.history)?e.history.length:0}},{kind:"server",sessionId:e.sessionId});case"worktree-diff":return d("worktree:diff:ready",{requestId:e.id||null,record:e.record||{branch:e.branch||null,summary:e.summary||null,previewEntrypoints:[{type:"worktree-diff",branch:e.branch||null}]}},{kind:"server"});case"worktree-merged":return d("worktree:merge:completed",{requestId:e.id||null,record:e.record||{branch:e.branch||null,summary:e.summary||null,conflicts:e.conflicts||[],previewEntrypoints:e.previewEntrypoints||[]}},{kind:"server"});case"compression-stats":return d("compression:summary",{requestId:e.id||null,summary:e.summary||{}},{kind:"server"});default:return null}}function N(e,n=null){C.forEach(i=>i(e,n||e))}function F(e){if(!e)return null;const n=e.record||{id:e.id||null,type:e.type||null,provider:e.provider||null,model:e.model||null,projectRoot:e.projectRoot||null,messageCount:e.messageCount??0,history:e.history||[],status:e.status||null};return{...e,id:e.id||n.id,type:e.type||n.type,provider:e.provider||n.provider,model:e.model||n.model,projectRoot:e.projectRoot||n.projectRoot,messageCount:e.messageCount??n.messageCount??0,status:e.status||n.status||null,record:n}}const Q=H("ws",()=>{const e=E(null),n=E("disconnected"),i=E(null),c=new Map,a=new Map;let b=null,w=1e3;const y=window.__CC_CONFIG__||{},I=z(()=>`ws://${y.wsHost||"127.0.0.1"}:${y.wsPort||18800}`);function h(){if(e.value?.readyState!==WebSocket.OPEN){n.value="connecting",i.value=null;try{const t=new WebSocket(I.value);e.value=t,t.onopen=()=>{w=1e3,y.wsToken?l({type:"auth",id:p(),token:y.wsToken}).then(()=>{n.value="connected"}).catch(()=>{n.value="connected"}):n.value="connected"},t.onmessage=o=>{let r;try{r=JSON.parse(o.data)}catch{return}T(r)},t.onerror=()=>{i.value="WebSocket error",n.value="error"},t.onclose=()=>{n.value="disconnected",e.value=null,c.forEach(({reject:o})=>o(new Error("WebSocket closed"))),c.clear(),b=setTimeout(()=>{w=Math.min(w*2,3e4),h()},w)}}catch(t){n.value="error",i.value=t.message}}}function O(){clearTimeout(b),e.value?.close(),e.value=null,n.value="disconnected"}function T(t){const{type:o,id:r}=t;let u=!1;if(r&&c.has(r)){const{resolve:m,reject:k,timeout:D}=c.get(r);clearTimeout(D),c.delete(r),u=!0,o==="error"?k(new Error(t.message||"Unknown error")):m(t)}const s=t.sessionId;s&&a.has(s)&&a.get(s).forEach(k=>k(t)),S.forEach(m=>m(t));const v=B(t);return v&&N(v,t),u}const S=new Set;function W(t){return S.add(t),()=>S.delete(t)}function P(t){return C.add(t),()=>C.delete(t)}function J(t,o){return a.has(t)||a.set(t,new Set),a.get(t).add(o),()=>{a.get(t)?.delete(o),a.get(t)?.size===0&&a.delete(t)}}function l(t,o=15e3){return new Promise((r,u)=>{if(e.value?.readyState!==WebSocket.OPEN){u(new Error("WebSocket not connected"));return}const s=t.id||p(),v=setTimeout(()=>{c.delete(s),u(new Error("Request timeout"))},o);c.set(s,{resolve:r,reject:u,timeout:v}),e.value.send(JSON.stringify({...t,id:s}))})}function f(t=8e3){return n.value==="connected"?Promise.resolve():new Promise((o,r)=>{const u=Date.now()+t,s=()=>{if(n.value==="connected")return o();if(n.value==="error"||Date.now()>=u)return r(new Error(`WS not ready: ${n.value}`));setTimeout(s,150)};n.value==="disconnected"&&h(),s()})}async function R(t,o=3e4){await f(8e3);const r=await l({type:"execute",command:t},o),u=r.output??r.stdout??"",s=r.stderr??"";return{output:u||s,exitCode:r.exitCode??0}}async function j(t,o=3e4){const{output:r,exitCode:u}=await R(t,o);if(u!==0)throw new Error(`Command failed: ${r}`);try{return JSON.parse(r.trim())}catch{const s=r.match(/\{[\s\S]*\}|\[[\s\S]*\]/);if(s)return JSON.parse(s[0]);throw new Error(`Invalid JSON output: ${r.slice(0,200)}`)}}async function M(t="chat",o=null){await f(8e3);const r=p();return(await l({type:"session-create",id:r,sessionType:t,projectRoot:o||y.projectRoot||null})).sessionId}function _(t,o){e.value?.readyState===WebSocket.OPEN&&e.value.send(JSON.stringify({type:"session-message",id:p(),sessionId:t,content:o}))}function $(t,o,r){e.value?.readyState===WebSocket.OPEN&&e.value.send(JSON.stringify({type:"session-answer",id:p(),sessionId:t,requestId:o,answer:r}))}async function x(){return await f(8e3),((await l({type:"session-list"},1e4)).sessions||[]).map(F).filter(Boolean)}async function q(t){try{await l({type:"session-close",sessionId:t},5e3),N(d("session:end",{sessionId:t},{kind:"server",sessionId:t}),{type:"result",sessionId:t,success:!0})}catch{}}async function A(t){return await f(8e3),await l({type:"session-resume",sessionId:t},1e4)}return{status:n,error:i,wsUrl:I,connect:h,disconnect:O,waitConnected:f,onMessage:W,onRuntimeEvent:P,onSession:J,sendRaw:l,execute:R,executeJson:j,createSession:M,resumeSession:A,sendSessionMessage:_,answerQuestion:$,listSessions:x,closeSession:q}});export{Q as u};
@@ -8,9 +8,9 @@
8
8
  // Injected by web-ui-server.js at serve time
9
9
  window.__CC_CONFIG__ = __CC_CONFIG_PLACEHOLDER__;
10
10
  </script>
11
- <script type="module" crossorigin src="./assets/index-DQ5xXK7O.js"></script>
11
+ <script type="module" crossorigin src="./assets/index-CF2CqPYX.js"></script>
12
12
  <link rel="modulepreload" crossorigin href="./assets/vendor-CN0Iv_qZ.js">
13
- <link rel="modulepreload" crossorigin href="./assets/antd-ChLPLhSn.js">
13
+ <link rel="modulepreload" crossorigin href="./assets/antd-CJSBocer.js">
14
14
  <link rel="stylesheet" crossorigin href="./assets/index-CyGyEIVX.css">
15
15
  </head>
16
16
  <body>
@@ -6,8 +6,7 @@
6
6
  * AI reads files, writes code, runs commands, and explains what it's doing.
7
7
  */
8
8
 
9
- import { startAgentRepl } from "../repl/agent-repl.js";
10
- import { loadConfig } from "../lib/config-manager.js";
9
+ import { createAgentRuntimeFactory } from "../runtime/runtime-factory.js";
11
10
 
12
11
  export function registerAgentCommand(program) {
13
12
  program
@@ -25,13 +24,13 @@ export function registerAgentCommand(program) {
25
24
  .option("--api-key <key>", "API key")
26
25
  .option("--session <id>", "Resume a previous agent session")
27
26
  .action(async (options) => {
28
- const config = loadConfig();
29
- await startAgentRepl({
30
- model: options.model || config.llm?.model || "qwen2:7b",
31
- provider: options.provider || config.llm?.provider || "ollama",
32
- baseUrl: options.baseUrl || config.llm?.baseUrl,
33
- apiKey: options.apiKey || config.llm?.apiKey,
27
+ const runtime = createAgentRuntimeFactory().createAgentRuntime({
28
+ model: options.model,
29
+ provider: options.provider,
30
+ baseUrl: options.baseUrl,
31
+ apiKey: options.apiKey,
34
32
  sessionId: options.session,
35
33
  });
34
+ await runtime.startAgentSession();
36
35
  });
37
36
  }
@@ -3,9 +3,7 @@
3
3
  * chainlesschain chat [--model] [--provider] [--agent]
4
4
  */
5
5
 
6
- import { startChatRepl } from "../repl/chat-repl.js";
7
- import { startAgentRepl } from "../repl/agent-repl.js";
8
- import { loadConfig } from "../lib/config-manager.js";
6
+ import { createAgentRuntimeFactory } from "../runtime/runtime-factory.js";
9
7
 
10
8
  export function registerChatCommand(program) {
11
9
  program
@@ -24,19 +22,19 @@ export function registerChatCommand(program) {
24
22
  )
25
23
  .option("--session <id>", "Resume a previous session (agent mode)")
26
24
  .action(async (options) => {
27
- const config = loadConfig();
28
- const replOptions = {
29
- model: options.model || config.llm?.model || "qwen2:7b",
30
- provider: options.provider || config.llm?.provider || "ollama",
31
- baseUrl: options.baseUrl || config.llm?.baseUrl,
32
- apiKey: options.apiKey || config.llm?.apiKey,
25
+ const factory = createAgentRuntimeFactory();
26
+ const runtimeOptions = {
27
+ model: options.model,
28
+ provider: options.provider,
29
+ baseUrl: options.baseUrl,
30
+ apiKey: options.apiKey,
33
31
  sessionId: options.session,
34
32
  };
35
33
 
36
34
  if (options.agent) {
37
- await startAgentRepl(replOptions);
35
+ await factory.createAgentRuntime(runtimeOptions).startAgentSession();
38
36
  } else {
39
- await startChatRepl(replOptions);
37
+ await factory.createChatRuntime(runtimeOptions).startChatSession();
40
38
  }
41
39
  });
42
40
  }
@@ -3,11 +3,8 @@
3
3
  * chainlesschain serve [--port] [--host] [--token] [--max-connections] [--timeout] [--allow-remote] [--project]
4
4
  */
5
5
 
6
- import chalk from "chalk";
7
6
  import { logger } from "../lib/logger.js";
8
- import { ChainlessChainWSServer } from "../lib/ws-server.js";
9
- import { WSSessionManager } from "../lib/ws-session-manager.js";
10
- import { bootstrap } from "../runtime/bootstrap.js";
7
+ import { createAgentRuntimeFactory } from "../runtime/runtime-factory.js";
11
8
 
12
9
  export function registerServeCommand(program) {
13
10
  program
@@ -31,109 +28,17 @@ export function registerServeCommand(program) {
31
28
  )
32
29
  .option("--project <path>", "Default project root for sessions")
33
30
  .action(async (opts) => {
34
- const port = parseInt(opts.port, 10);
35
- const maxConnections = parseInt(opts.maxConnections, 10);
36
- const timeout = parseInt(opts.timeout, 10);
37
- let host = opts.host;
38
-
39
- // Validation
40
- if (isNaN(port) || port < 1 || port > 65535) {
41
- logger.error("Invalid port number. Must be between 1 and 65535.");
42
- process.exit(1);
43
- }
44
-
45
- if (opts.allowRemote) {
46
- if (!opts.token) {
47
- logger.error("--allow-remote requires --token for security.");
48
- process.exit(1);
49
- }
50
- host = "0.0.0.0";
51
- }
52
-
53
- // Bootstrap headless runtime for DB access
54
- let db = null;
55
- try {
56
- const ctx = await bootstrap({ skipDb: false });
57
- db = ctx.db?.getDb?.() || null;
58
- } catch (_err) {
59
- logger.log(
60
- chalk.yellow(
61
- " Warning: Database not available, sessions will be in-memory only",
62
- ),
63
- );
64
- }
65
-
66
- // Create session manager
67
- const sessionManager = new WSSessionManager({
68
- db,
69
- defaultProjectRoot: opts.project || process.cwd(),
70
- });
71
-
72
- const server = new ChainlessChainWSServer({
73
- port,
74
- host,
75
- token: opts.token || null,
76
- maxConnections,
77
- timeout,
78
- sessionManager,
79
- });
80
-
81
- // Event logging
82
- server.on("connection", ({ clientId, ip }) => {
83
- logger.log(chalk.green(` + Client connected: ${clientId} (${ip})`));
84
- });
85
-
86
- server.on("disconnection", ({ clientId, reason }) => {
87
- const extra = reason ? ` (${reason})` : "";
88
- logger.log(
89
- chalk.yellow(` - Client disconnected: ${clientId}${extra}`),
90
- );
91
- });
92
-
93
- server.on("command:start", ({ id, command }) => {
94
- logger.log(chalk.cyan(` > [${id}] ${command}`));
95
- });
96
-
97
- server.on("command:end", ({ id, exitCode }) => {
98
- const color = exitCode === 0 ? chalk.green : chalk.red;
99
- logger.log(color(` < [${id}] exit ${exitCode}`));
100
- });
101
-
102
- server.on("session:create", ({ sessionId, type }) => {
103
- logger.log(chalk.green(` + Session created: ${sessionId} (${type})`));
104
- });
105
-
106
- server.on("session:close", ({ sessionId }) => {
107
- logger.log(chalk.yellow(` - Session closed: ${sessionId}`));
108
- });
109
-
110
- // Graceful shutdown
111
- const shutdown = async () => {
112
- logger.log("\n" + chalk.yellow("Shutting down WebSocket server..."));
113
- await server.stop();
114
- process.exit(0);
115
- };
116
-
117
- process.on("SIGINT", shutdown);
118
- process.on("SIGTERM", shutdown);
119
-
120
31
  try {
121
- await server.start();
122
-
123
- logger.log("");
124
- logger.log(chalk.bold(" ChainlessChain WebSocket Server"));
125
- logger.log("");
126
- logger.log(` Address: ${chalk.cyan(`ws://${host}:${port}`)}`);
127
- logger.log(
128
- ` Auth: ${opts.token ? chalk.green("enabled") : chalk.yellow("disabled")}`,
129
- );
130
- logger.log(` Sessions: ${chalk.green("enabled")}`);
131
- logger.log(` Project: ${opts.project || process.cwd()}`);
132
- logger.log(` Max conn: ${maxConnections}`);
133
- logger.log(` Timeout: ${timeout}ms`);
134
- logger.log("");
135
- logger.log(chalk.dim(" Press Ctrl+C to stop"));
136
- logger.log("");
32
+ const runtime = createAgentRuntimeFactory().createServerRuntime({
33
+ port: parseInt(opts.port, 10),
34
+ host: opts.host,
35
+ token: opts.token,
36
+ maxConnections: parseInt(opts.maxConnections, 10),
37
+ timeout: parseInt(opts.timeout, 10),
38
+ allowRemote: opts.allowRemote,
39
+ project: opts.project,
40
+ });
41
+ await runtime.startServer();
137
42
  } catch (err) {
138
43
  logger.error(`Failed to start server: ${err.message}`);
139
44
  process.exit(1);
@@ -13,6 +13,17 @@ import {
13
13
  deleteSession,
14
14
  exportSessionMarkdown,
15
15
  } from "../lib/session-manager.js";
16
+ import {
17
+ listJsonlSessions,
18
+ rebuildMessages,
19
+ sessionExists,
20
+ readEvents,
21
+ migrateLegacySessions,
22
+ migrateLegacySessionsBatch,
23
+ validateJsonlSession,
24
+ validateAllJsonlSessions,
25
+ } from "../lib/jsonl-session-store.js";
26
+ import { feature } from "../lib/feature-flags.js";
16
27
 
17
28
  export function registerSessionCommand(program) {
18
29
  const session = program
@@ -28,14 +39,39 @@ export function registerSessionCommand(program) {
28
39
  .action(async (options) => {
29
40
  try {
30
41
  const ctx = await bootstrap({ verbose: program.opts().verbose });
31
- if (!ctx.db) {
32
- logger.error("Database not available");
33
- process.exit(1);
42
+ const limit = Math.max(1, parseInt(options.limit) || 20);
43
+ let sessions = [];
44
+
45
+ // Merge DB sessions + JSONL sessions
46
+ if (ctx.db) {
47
+ const db = ctx.db.getDatabase();
48
+ sessions.push(
49
+ ...listSessions(db, { limit }).map((s) => ({
50
+ ...s,
51
+ _store: "db",
52
+ })),
53
+ );
34
54
  }
35
- const db = ctx.db.getDatabase();
36
- const sessions = listSessions(db, {
37
- limit: Math.max(1, parseInt(options.limit) || 20),
38
- });
55
+
56
+ if (feature("JSONL_SESSION")) {
57
+ sessions.push(
58
+ ...listJsonlSessions({ limit }).map((s) => ({
59
+ ...s,
60
+ _store: "jsonl",
61
+ })),
62
+ );
63
+ }
64
+
65
+ // Deduplicate by id (JSONL takes precedence), sort by updated_at
66
+ const seen = new Set();
67
+ sessions = sessions
68
+ .sort((a, b) => (b.updated_at > a.updated_at ? 1 : -1))
69
+ .filter((s) => {
70
+ if (seen.has(s.id)) return false;
71
+ seen.add(s.id);
72
+ return true;
73
+ })
74
+ .slice(0, limit);
39
75
 
40
76
  if (options.json) {
41
77
  console.log(JSON.stringify(sessions, null, 2));
@@ -46,8 +82,10 @@ export function registerSessionCommand(program) {
46
82
  } else {
47
83
  logger.log(chalk.bold(`Sessions (${sessions.length}):\n`));
48
84
  for (const s of sessions) {
85
+ const storeTag =
86
+ s._store === "jsonl" ? chalk.yellow("[JSONL]") : "";
49
87
  logger.log(
50
- ` ${chalk.gray(s.id.slice(0, 16))} ${chalk.white(s.title)} ${chalk.cyan(s.message_count + " msgs")} ${chalk.gray(s.updated_at)}`,
88
+ ` ${chalk.gray(s.id.slice(0, 16))} ${chalk.white(s.title)} ${chalk.cyan(s.message_count + " msgs")} ${chalk.gray(s.updated_at)} ${storeTag}`,
51
89
  );
52
90
  if (s.summary) {
53
91
  logger.log(` ${chalk.gray(s.summary.substring(0, 100))}`);
@@ -72,12 +110,29 @@ export function registerSessionCommand(program) {
72
110
  .action(async (id, options) => {
73
111
  try {
74
112
  const ctx = await bootstrap({ verbose: program.opts().verbose });
75
- if (!ctx.db) {
76
- logger.error("Database not available");
77
- process.exit(1);
113
+ let sess = null;
114
+
115
+ // Try JSONL first if enabled
116
+ if (feature("JSONL_SESSION") && sessionExists(id)) {
117
+ const events = readEvents(id);
118
+ const startEvent = events.find((e) => e.type === "session_start");
119
+ const msgs = rebuildMessages(id);
120
+ sess = {
121
+ id,
122
+ title: startEvent?.data?.title || "Untitled",
123
+ provider: startEvent?.data?.provider || "",
124
+ model: startEvent?.data?.model || "",
125
+ message_count: msgs.length,
126
+ messages: msgs,
127
+ _store: "jsonl",
128
+ };
129
+ }
130
+
131
+ // Fallback to DB
132
+ if (!sess && ctx.db) {
133
+ const db = ctx.db.getDatabase();
134
+ sess = getSession(db, id);
78
135
  }
79
- const db = ctx.db.getDatabase();
80
- const sess = getSession(db, id);
81
136
 
82
137
  if (!sess) {
83
138
  logger.error(`Session not found: ${id}`);
@@ -127,12 +182,27 @@ export function registerSessionCommand(program) {
127
182
  .action(async (id, options) => {
128
183
  try {
129
184
  const ctx = await bootstrap({ verbose: program.opts().verbose });
130
- if (!ctx.db) {
131
- logger.error("Database not available");
132
- process.exit(1);
185
+ let sess = null;
186
+
187
+ // Try JSONL first
188
+ if (feature("JSONL_SESSION") && sessionExists(id)) {
189
+ const events = readEvents(id);
190
+ const startEvent = events.find((e) => e.type === "session_start");
191
+ sess = {
192
+ id,
193
+ title: startEvent?.data?.title || "Untitled",
194
+ provider: startEvent?.data?.provider || "",
195
+ model: startEvent?.data?.model || "",
196
+ messages: rebuildMessages(id),
197
+ };
198
+ sess.message_count = sess.messages.length;
199
+ }
200
+
201
+ // Fallback to DB
202
+ if (!sess && ctx.db) {
203
+ const db = ctx.db.getDatabase();
204
+ sess = getSession(db, id);
133
205
  }
134
- const db = ctx.db.getDatabase();
135
- const sess = getSession(db, id);
136
206
 
137
207
  if (!sess) {
138
208
  logger.error(`Session not found: ${id}`);
@@ -235,4 +305,101 @@ export function registerSessionCommand(program) {
235
305
  process.exit(1);
236
306
  }
237
307
  });
308
+
309
+ session
310
+ .command("migrate")
311
+ .description("Migrate legacy JSON session files to JSONL")
312
+ .argument("[source]", "Directory containing legacy .json session files")
313
+ .option("--dry-run", "Show what would migrate without writing files")
314
+ .option("--force", "Overwrite existing JSONL sessions")
315
+ .option("--no-archive", "Do not keep .migrated.json backups")
316
+ .option("--sample-size <n>", "Validate N migrated sessions after migration", "3")
317
+ .option("--retry-failures", "Retry failed migrations once")
318
+ .option("--json", "Output as JSON")
319
+ .action(async (source, options) => {
320
+ try {
321
+ const report = migrateLegacySessionsBatch(source, {
322
+ dryRun: options.dryRun,
323
+ force: options.force,
324
+ archive: options.archive,
325
+ sampleSize: parseInt(options.sampleSize, 10) || 3,
326
+ retryFailures: options.retryFailures,
327
+ });
328
+ const results = report.results || migrateLegacySessions(source, options);
329
+
330
+ if (options.json) {
331
+ console.log(JSON.stringify(report, null, 2));
332
+ return;
333
+ }
334
+
335
+ if (results.length === 0) {
336
+ logger.info("No legacy JSON session files found.");
337
+ return;
338
+ }
339
+
340
+ for (const result of results) {
341
+ if (result.skipped) {
342
+ logger.log(
343
+ `${chalk.yellow("skip")} ${result.file} -> ${result.sessionId} (${result.reason})`,
344
+ );
345
+ continue;
346
+ }
347
+ logger.log(
348
+ `${chalk.green(options.dryRun ? "plan" : "migrated")} ${result.file} -> ${result.sessionId} (${result.messageCount} messages)`,
349
+ );
350
+ }
351
+
352
+ logger.log(
353
+ chalk.gray(
354
+ `summary: scanned ${report.summary.scanned}, migrated ${report.summary.migrated}, skipped ${report.summary.skipped}, failed ${report.summary.failed}, retries ${report.summary.retries}`,
355
+ ),
356
+ );
357
+
358
+ if (report.sampledValidation?.length) {
359
+ for (const item of report.sampledValidation) {
360
+ const label = item.valid && item.matchesExpectedMessages
361
+ ? chalk.green("sample-ok")
362
+ : chalk.red("sample-fail");
363
+ logger.log(
364
+ `${label} ${item.sessionId} (${item.messageCount}/${item.expectedMessageCount} messages)`,
365
+ );
366
+ }
367
+ }
368
+ } catch (err) {
369
+ logger.error(`Failed: ${err.message}`);
370
+ process.exit(1);
371
+ }
372
+ });
373
+
374
+ session
375
+ .command("validate")
376
+ .description("Validate JSONL session files")
377
+ .argument("[id]", "Session ID to validate")
378
+ .option("--json", "Output as JSON")
379
+ .action(async (id, options) => {
380
+ try {
381
+ const result = id
382
+ ? validateJsonlSession(id)
383
+ : validateAllJsonlSessions();
384
+
385
+ if (options.json) {
386
+ console.log(JSON.stringify(result, null, 2));
387
+ return;
388
+ }
389
+
390
+ const results = Array.isArray(result) ? result : [result];
391
+ for (const item of results) {
392
+ const label = item.valid ? chalk.green("valid") : chalk.red("invalid");
393
+ logger.log(
394
+ `${label} ${item.sessionId} (${item.eventCount} events, ${item.messageCount || 0} messages, malformed: ${item.malformedLines})`,
395
+ );
396
+ if (!item.valid && item.reason) {
397
+ logger.log(` ${chalk.gray(item.reason)}`);
398
+ }
399
+ }
400
+ } catch (err) {
401
+ logger.error(`Failed: ${err.message}`);
402
+ process.exit(1);
403
+ }
404
+ });
238
405
  }
@@ -1,39 +1,5 @@
1
- /**
2
- * ui command — start a local web management UI
3
- * chainlesschain ui [--port] [--ws-port] [--host] [--no-open] [--token]
4
- *
5
- * Project mode (run from a dir with .chainlesschain/): project-scoped chat UI
6
- * Global mode (run from any other dir): global management panel
7
- */
8
-
9
- import { execSync } from "child_process";
10
- import path from "path";
11
- import chalk from "chalk";
12
1
  import { logger } from "../lib/logger.js";
13
- import { ChainlessChainWSServer } from "../lib/ws-server.js";
14
- import { WSSessionManager } from "../lib/ws-session-manager.js";
15
- import { createWebUIServer } from "../lib/web-ui-server.js";
16
- import { bootstrap } from "../runtime/bootstrap.js";
17
- import { findProjectRoot, loadProjectConfig } from "../lib/project-detector.js";
18
- import { loadConfig } from "../lib/config-manager.js";
19
-
20
- /**
21
- * Open a URL in the system default browser (cross-platform).
22
- */
23
- function openBrowser(url) {
24
- try {
25
- const platform = process.platform;
26
- if (platform === "win32") {
27
- execSync(`start "" "${url}"`, { stdio: "ignore" });
28
- } else if (platform === "darwin") {
29
- execSync(`open "${url}"`, { stdio: "ignore" });
30
- } else {
31
- execSync(`xdg-open "${url}"`, { stdio: "ignore" });
32
- }
33
- } catch (_err) {
34
- // Non-critical — user can open manually
35
- }
36
- }
2
+ import { createAgentRuntimeFactory } from "../runtime/runtime-factory.js";
37
3
 
38
4
  export function registerUiCommand(program) {
39
5
  program
@@ -52,126 +18,19 @@ export function registerUiCommand(program) {
52
18
  "Path to built web-panel dist/ directory (auto-detected by default)",
53
19
  )
54
20
  .action(async (opts) => {
55
- const httpPort = parseInt(opts.port, 10);
56
- const wsPort = parseInt(opts.wsPort, 10);
57
- const host = opts.host;
58
-
59
- if (isNaN(httpPort) || httpPort < 1 || httpPort > 65535) {
60
- logger.error("Invalid --port. Must be between 1 and 65535.");
61
- process.exit(1);
62
- }
63
- if (isNaN(wsPort) || wsPort < 1 || wsPort > 65535) {
64
- logger.error("Invalid --ws-port. Must be between 1 and 65535.");
65
- process.exit(1);
66
- }
67
-
68
- // ── Detect project context ────────────────────────────────────────────
69
- const projectRoot = findProjectRoot(process.cwd());
70
- const projectConfig = projectRoot ? loadProjectConfig(projectRoot) : null;
71
- const projectName =
72
- projectConfig?.name ||
73
- (projectRoot ? path.basename(projectRoot) : null);
74
- const mode = projectRoot ? "project" : "global";
75
-
76
- // ── Bootstrap headless runtime ────────────────────────────────────────
77
- let db = null;
78
- try {
79
- const ctx = await bootstrap({ skipDb: false });
80
- db = ctx.db?.getDb?.() || null;
81
- } catch (_err) {
82
- logger.log(
83
- chalk.yellow(
84
- " Warning: Database not available, sessions will be in-memory only",
85
- ),
86
- );
87
- }
88
-
89
- // ── Start WebSocket server ────────────────────────────────────────────
90
- const appConfig = loadConfig();
91
- const sessionManager = new WSSessionManager({
92
- db,
93
- defaultProjectRoot: projectRoot || process.cwd(),
94
- config: appConfig,
95
- });
96
-
97
- const wsServer = new ChainlessChainWSServer({
98
- port: wsPort,
99
- host,
100
- token: opts.token || null,
101
- maxConnections: 20,
102
- timeout: 60000,
103
- sessionManager,
104
- });
105
-
106
- try {
107
- await wsServer.start();
108
- } catch (err) {
109
- logger.error(`Failed to start WebSocket server: ${err.message}`);
110
- process.exit(1);
111
- }
112
-
113
- // ── Start HTTP server ─────────────────────────────────────────────────
114
- const httpServer = createWebUIServer({
115
- wsPort,
116
- wsToken: opts.token || null,
117
- wsHost: host === "0.0.0.0" ? "127.0.0.1" : host,
118
- projectRoot,
119
- projectName,
120
- mode,
121
- staticDir: opts.webPanelDir || null,
122
- });
123
-
124
21
  try {
125
- await new Promise((resolve, reject) => {
126
- httpServer.listen(httpPort, host, () => resolve());
127
- httpServer.on("error", reject);
22
+ const runtime = createAgentRuntimeFactory().createUiRuntime({
23
+ port: parseInt(opts.port, 10),
24
+ wsPort: parseInt(opts.wsPort, 10),
25
+ host: opts.host,
26
+ open: opts.open,
27
+ token: opts.token || null,
28
+ webPanelDir: opts.webPanelDir || null,
128
29
  });
30
+ await runtime.startUiServer();
129
31
  } catch (err) {
130
- logger.error(`Failed to start HTTP server: ${err.message}`);
32
+ logger.error(`Failed to start UI server: ${err.message}`);
131
33
  process.exit(1);
132
34
  }
133
-
134
- // ── Print startup info ────────────────────────────────────────────────
135
- const uiUrl = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${httpPort}`;
136
-
137
- logger.log("");
138
- logger.log(chalk.bold(" ChainlessChain 管理面板"));
139
- logger.log("");
140
- if (mode === "project") {
141
- logger.log(
142
- ` Mode: ${chalk.cyan("project")} ${chalk.dim(projectRoot)}`,
143
- );
144
- if (projectName) {
145
- logger.log(` Project: ${chalk.green(projectName)}`);
146
- }
147
- } else {
148
- logger.log(` Mode: ${chalk.cyan("global")}`);
149
- }
150
- logger.log(` UI: ${chalk.cyan(uiUrl)}`);
151
- logger.log(` WS: ${chalk.dim(`ws://${host}:${wsPort}`)}`);
152
- logger.log(
153
- ` Auth: ${opts.token ? chalk.green("enabled") : chalk.yellow("disabled")}`,
154
- );
155
- logger.log("");
156
- logger.log(chalk.dim(" Press Ctrl+C to stop"));
157
- logger.log("");
158
-
159
- // ── Open browser ──────────────────────────────────────────────────────
160
- if (opts.open !== false) {
161
- openBrowser(uiUrl);
162
- }
163
-
164
- // ── Graceful shutdown ─────────────────────────────────────────────────
165
- const shutdown = async () => {
166
- logger.log("\n" + chalk.yellow("Shutting down UI server..."));
167
- await Promise.all([
168
- new Promise((resolve) => httpServer.close(resolve)),
169
- wsServer.stop(),
170
- ]);
171
- process.exit(0);
172
- };
173
-
174
- process.on("SIGINT", shutdown);
175
- process.on("SIGTERM", shutdown);
176
35
  });
177
36
  }
@@ -0,0 +1 @@
1
+ export { startAgentRepl } from "../../repl/agent-repl.js";