@phenx-inc/ctlsurf 0.1.0

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 (133) hide show
  1. package/bin/ctlsurf-worker.js +173 -0
  2. package/electron-vite.config.ts +34 -0
  3. package/out/headless/index.mjs +1364 -0
  4. package/out/headless/index.mjs.map +7 -0
  5. package/out/main/index.js +1131 -0
  6. package/out/preload/index.js +67 -0
  7. package/out/renderer/assets/abap-D5KwWAsZ.js +1399 -0
  8. package/out/renderer/assets/apex-DVGUZ64i.js +331 -0
  9. package/out/renderer/assets/azcli-BEAhqcuE.js +69 -0
  10. package/out/renderer/assets/bat-Bqkp9Cfu.js +101 -0
  11. package/out/renderer/assets/bicep-DIlfshcM.js +110 -0
  12. package/out/renderer/assets/cameligo-CLaaYNMV.js +175 -0
  13. package/out/renderer/assets/clojure-fcgFaMHx.js +762 -0
  14. package/out/renderer/assets/codicon-ngg6Pgfi.ttf +0 -0
  15. package/out/renderer/assets/coffee-CzJ5oEdj.js +233 -0
  16. package/out/renderer/assets/cpp-CcN6f0ik.js +390 -0
  17. package/out/renderer/assets/csharp-BJeIuvde.js +327 -0
  18. package/out/renderer/assets/csp-D_3BK2Wp.js +54 -0
  19. package/out/renderer/assets/css-i3rI3_64.js +186 -0
  20. package/out/renderer/assets/css.worker-umuuUiIb.js +53567 -0
  21. package/out/renderer/assets/cssMode-DL0XItGB.js +208 -0
  22. package/out/renderer/assets/cypher-D0--_GAN.js +264 -0
  23. package/out/renderer/assets/dart-vLMHv35g.js +282 -0
  24. package/out/renderer/assets/dockerfile--oxj0cAH.js +131 -0
  25. package/out/renderer/assets/ecl-CeuUgzaZ.js +457 -0
  26. package/out/renderer/assets/editor.worker-CNgWLVu7.js +13695 -0
  27. package/out/renderer/assets/elixir-eLfY1jWH.js +570 -0
  28. package/out/renderer/assets/flow9-ZSTChSMd.js +143 -0
  29. package/out/renderer/assets/freemarker2-CrOEuDcF.js +995 -0
  30. package/out/renderer/assets/fsharp-D2uoxuLH.js +218 -0
  31. package/out/renderer/assets/go-brnMpFrj.js +219 -0
  32. package/out/renderer/assets/graphql-BeiGgjIU.js +152 -0
  33. package/out/renderer/assets/handlebars-D4QYaBof.js +414 -0
  34. package/out/renderer/assets/hcl-CrX1Es2W.js +184 -0
  35. package/out/renderer/assets/html-B2Dqk2ai.js +303 -0
  36. package/out/renderer/assets/html.worker-BT47iy49.js +29777 -0
  37. package/out/renderer/assets/htmlMode-CdZ0Prhd.js +224 -0
  38. package/out/renderer/assets/index-CJ6RsQWP.css +8108 -0
  39. package/out/renderer/assets/index-pZmE1QXB.js +211777 -0
  40. package/out/renderer/assets/ini-BcQysCTb.js +72 -0
  41. package/out/renderer/assets/java-Dt3iMn2o.js +233 -0
  42. package/out/renderer/assets/javascript-CK8zNQXj.js +72 -0
  43. package/out/renderer/assets/json.worker-D4JVmXIe.js +21424 -0
  44. package/out/renderer/assets/jsonMode-Cewaellc.js +931 -0
  45. package/out/renderer/assets/julia-Cm3ItYL_.js +512 -0
  46. package/out/renderer/assets/kotlin-Ddo1SjA5.js +253 -0
  47. package/out/renderer/assets/less-B7Qaxw-O.js +162 -0
  48. package/out/renderer/assets/lexon-C1U0m2n9.js +158 -0
  49. package/out/renderer/assets/liquid-Bd3GPNs2.js +235 -0
  50. package/out/renderer/assets/lspLanguageFeatures-DSDH7BnA.js +1841 -0
  51. package/out/renderer/assets/lua-hNsuGJkO.js +163 -0
  52. package/out/renderer/assets/m3-6ko6q9-_.js +211 -0
  53. package/out/renderer/assets/markdown-B0YTnTxW.js +230 -0
  54. package/out/renderer/assets/mdx-CCPVCrXC.js +159 -0
  55. package/out/renderer/assets/mips-CJm71dS3.js +199 -0
  56. package/out/renderer/assets/msdax-BBeIktCY.js +376 -0
  57. package/out/renderer/assets/mysql-BWiizXSn.js +879 -0
  58. package/out/renderer/assets/objective-c-B1L1C5EC.js +184 -0
  59. package/out/renderer/assets/pascal-DMQyD4Xk.js +252 -0
  60. package/out/renderer/assets/pascaligo-VA_LQ1oU.js +165 -0
  61. package/out/renderer/assets/perl-DC0Z0tlO.js +627 -0
  62. package/out/renderer/assets/pgsql-DaSGFTLp.js +852 -0
  63. package/out/renderer/assets/php-Bkx1qpkQ.js +501 -0
  64. package/out/renderer/assets/pla-DEV89yYj.js +138 -0
  65. package/out/renderer/assets/postiats-CVVurEnu.js +908 -0
  66. package/out/renderer/assets/powerquery-BQ_t1ZiQ.js +891 -0
  67. package/out/renderer/assets/powershell-BXiKvz7Z.js +240 -0
  68. package/out/renderer/assets/protobuf-CndvAUGu.js +421 -0
  69. package/out/renderer/assets/pug-BxCXwerb.js +403 -0
  70. package/out/renderer/assets/python-34jOtlcC.js +295 -0
  71. package/out/renderer/assets/qsharp-BWK6YLKm.js +302 -0
  72. package/out/renderer/assets/r-CtqYUQ6l.js +244 -0
  73. package/out/renderer/assets/razor-DXRw694z.js +545 -0
  74. package/out/renderer/assets/redis-O7gSt3oh.js +303 -0
  75. package/out/renderer/assets/redshift-CvYMMYZY.js +810 -0
  76. package/out/renderer/assets/restructuredtext-B-KQCVu_.js +175 -0
  77. package/out/renderer/assets/ruby-DCd4DmAr.js +512 -0
  78. package/out/renderer/assets/rust-B1c0VCeq.js +344 -0
  79. package/out/renderer/assets/sb-Chfc_wZF.js +116 -0
  80. package/out/renderer/assets/scala-DbVzH-3O.js +371 -0
  81. package/out/renderer/assets/scheme-D7PxodDG.js +109 -0
  82. package/out/renderer/assets/scss-B42qMyAu.js +261 -0
  83. package/out/renderer/assets/shell-vZEubQ82.js +222 -0
  84. package/out/renderer/assets/solidity-yHOxYChb.js +1368 -0
  85. package/out/renderer/assets/sophia-D7pU0Y1d.js +200 -0
  86. package/out/renderer/assets/sparql-DxuVdnRl.js +202 -0
  87. package/out/renderer/assets/sql-BAGepFCR.js +854 -0
  88. package/out/renderer/assets/st-C-b0Dh53.js +417 -0
  89. package/out/renderer/assets/swift-BmOZGynf.js +313 -0
  90. package/out/renderer/assets/systemverilog-BOC0OOdC.js +577 -0
  91. package/out/renderer/assets/tcl-Bb4GCwBr.js +233 -0
  92. package/out/renderer/assets/ts.worker-C7hW3aY-.js +225330 -0
  93. package/out/renderer/assets/tsMode-CmND5_wB.js +1265 -0
  94. package/out/renderer/assets/twig-DvgEGWAV.js +393 -0
  95. package/out/renderer/assets/typescript-BNNI0Euv.js +337 -0
  96. package/out/renderer/assets/typespec-R77Ln7Jb.js +128 -0
  97. package/out/renderer/assets/vb-Bm6ESA0Q.js +373 -0
  98. package/out/renderer/assets/wgsl-_KPae5vw.js +454 -0
  99. package/out/renderer/assets/xml-CgdndrNB.js +89 -0
  100. package/out/renderer/assets/yaml-DNWPIf1s.js +200 -0
  101. package/out/renderer/index.html +13 -0
  102. package/package.json +67 -0
  103. package/resources/icon.icns +0 -0
  104. package/resources/icon.ico +0 -0
  105. package/resources/icon.png +0 -0
  106. package/src/main/agents.ts +46 -0
  107. package/src/main/bridge.ts +180 -0
  108. package/src/main/ctlsurfApi.ts +142 -0
  109. package/src/main/detectMode.ts +17 -0
  110. package/src/main/headless.ts +182 -0
  111. package/src/main/index.ts +300 -0
  112. package/src/main/orchestrator.ts +404 -0
  113. package/src/main/pty.ts +65 -0
  114. package/src/main/settingsDir.ts +17 -0
  115. package/src/main/tui.ts +366 -0
  116. package/src/main/workerWs.ts +312 -0
  117. package/src/preload/index.ts +114 -0
  118. package/src/renderer/App.tsx +275 -0
  119. package/src/renderer/components/CtlsurfPanel.tsx +49 -0
  120. package/src/renderer/components/EditorPanel.tsx +232 -0
  121. package/src/renderer/components/MultiSplitPane.tsx +251 -0
  122. package/src/renderer/components/PaneLayout.tsx +419 -0
  123. package/src/renderer/components/SettingsDialog.tsx +204 -0
  124. package/src/renderer/components/SplitPane.tsx +82 -0
  125. package/src/renderer/components/StatusBar.tsx +73 -0
  126. package/src/renderer/components/TerminalPanel.tsx +140 -0
  127. package/src/renderer/index.html +12 -0
  128. package/src/renderer/main.tsx +10 -0
  129. package/src/renderer/styles.css +722 -0
  130. package/tsconfig.json +8 -0
  131. package/tsconfig.main.json +15 -0
  132. package/tsconfig.preload.json +14 -0
  133. package/tsconfig.renderer.json +15 -0
@@ -0,0 +1,200 @@
1
+ import { l as languages } from "./index-pZmE1QXB.js";
2
+ const conf = {
3
+ comments: {
4
+ lineComment: "#"
5
+ },
6
+ brackets: [
7
+ ["{", "}"],
8
+ ["[", "]"],
9
+ ["(", ")"]
10
+ ],
11
+ autoClosingPairs: [
12
+ { open: "{", close: "}" },
13
+ { open: "[", close: "]" },
14
+ { open: "(", close: ")" },
15
+ { open: '"', close: '"' },
16
+ { open: "'", close: "'" }
17
+ ],
18
+ surroundingPairs: [
19
+ { open: "{", close: "}" },
20
+ { open: "[", close: "]" },
21
+ { open: "(", close: ")" },
22
+ { open: '"', close: '"' },
23
+ { open: "'", close: "'" }
24
+ ],
25
+ folding: {
26
+ offSide: true
27
+ },
28
+ onEnterRules: [
29
+ {
30
+ beforeText: /:\s*$/,
31
+ action: {
32
+ indentAction: languages.IndentAction.Indent
33
+ }
34
+ }
35
+ ]
36
+ };
37
+ const language = {
38
+ tokenPostfix: ".yaml",
39
+ brackets: [
40
+ { token: "delimiter.bracket", open: "{", close: "}" },
41
+ { token: "delimiter.square", open: "[", close: "]" }
42
+ ],
43
+ keywords: ["true", "True", "TRUE", "false", "False", "FALSE", "null", "Null", "Null", "~"],
44
+ numberInteger: /(?:0|[+-]?[0-9]+)/,
45
+ numberFloat: /(?:0|[+-]?[0-9]+)(?:\.[0-9]+)?(?:e[-+][1-9][0-9]*)?/,
46
+ numberOctal: /0o[0-7]+/,
47
+ numberHex: /0x[0-9a-fA-F]+/,
48
+ numberInfinity: /[+-]?\.(?:inf|Inf|INF)/,
49
+ numberNaN: /\.(?:nan|Nan|NAN)/,
50
+ numberDate: /\d{4}-\d\d-\d\d([Tt ]\d\d:\d\d:\d\d(\.\d+)?(( ?[+-]\d\d?(:\d\d)?)|Z)?)?/,
51
+ escapes: /\\(?:[btnfr\\"']|[0-7][0-7]?|[0-3][0-7]{2})/,
52
+ tokenizer: {
53
+ root: [
54
+ { include: "@whitespace" },
55
+ { include: "@comment" },
56
+ // Directive
57
+ [/%[^ ]+.*$/, "meta.directive"],
58
+ // Document Markers
59
+ [/---/, "operators.directivesEnd"],
60
+ [/\.{3}/, "operators.documentEnd"],
61
+ // Block Structure Indicators
62
+ [/[-?:](?= )/, "operators"],
63
+ { include: "@anchor" },
64
+ { include: "@tagHandle" },
65
+ { include: "@flowCollections" },
66
+ { include: "@blockStyle" },
67
+ // Numbers
68
+ [/@numberInteger(?![ \t]*\S+)/, "number"],
69
+ [/@numberFloat(?![ \t]*\S+)/, "number.float"],
70
+ [/@numberOctal(?![ \t]*\S+)/, "number.octal"],
71
+ [/@numberHex(?![ \t]*\S+)/, "number.hex"],
72
+ [/@numberInfinity(?![ \t]*\S+)/, "number.infinity"],
73
+ [/@numberNaN(?![ \t]*\S+)/, "number.nan"],
74
+ [/@numberDate(?![ \t]*\S+)/, "number.date"],
75
+ // Key:Value pair
76
+ [/(".*?"|'.*?'|[^#'"]*?)([ \t]*)(:)( |$)/, ["type", "white", "operators", "white"]],
77
+ { include: "@flowScalars" },
78
+ // String nodes
79
+ [
80
+ /.+?(?=(\s+#|$))/,
81
+ {
82
+ cases: {
83
+ "@keywords": "keyword",
84
+ "@default": "string"
85
+ }
86
+ }
87
+ ]
88
+ ],
89
+ // Flow Collection: Flow Mapping
90
+ object: [
91
+ { include: "@whitespace" },
92
+ { include: "@comment" },
93
+ // Flow Mapping termination
94
+ [/\}/, "@brackets", "@pop"],
95
+ // Flow Mapping delimiter
96
+ [/,/, "delimiter.comma"],
97
+ // Flow Mapping Key:Value delimiter
98
+ [/:(?= )/, "operators"],
99
+ // Flow Mapping Key:Value key
100
+ [/(?:".*?"|'.*?'|[^,\{\[]+?)(?=: )/, "type"],
101
+ // Start Flow Style
102
+ { include: "@flowCollections" },
103
+ { include: "@flowScalars" },
104
+ // Scalar Data types
105
+ { include: "@tagHandle" },
106
+ { include: "@anchor" },
107
+ { include: "@flowNumber" },
108
+ // Other value (keyword or string)
109
+ [
110
+ /[^\},]+/,
111
+ {
112
+ cases: {
113
+ "@keywords": "keyword",
114
+ "@default": "string"
115
+ }
116
+ }
117
+ ]
118
+ ],
119
+ // Flow Collection: Flow Sequence
120
+ array: [
121
+ { include: "@whitespace" },
122
+ { include: "@comment" },
123
+ // Flow Sequence termination
124
+ [/\]/, "@brackets", "@pop"],
125
+ // Flow Sequence delimiter
126
+ [/,/, "delimiter.comma"],
127
+ // Start Flow Style
128
+ { include: "@flowCollections" },
129
+ { include: "@flowScalars" },
130
+ // Scalar Data types
131
+ { include: "@tagHandle" },
132
+ { include: "@anchor" },
133
+ { include: "@flowNumber" },
134
+ // Other value (keyword or string)
135
+ [
136
+ /[^\],]+/,
137
+ {
138
+ cases: {
139
+ "@keywords": "keyword",
140
+ "@default": "string"
141
+ }
142
+ }
143
+ ]
144
+ ],
145
+ // First line of a Block Style
146
+ multiString: [[/^( +).+$/, "string", "@multiStringContinued.$1"]],
147
+ // Further lines of a Block Style
148
+ // Workaround for indentation detection
149
+ multiStringContinued: [
150
+ [
151
+ /^( *).+$/,
152
+ {
153
+ cases: {
154
+ "$1==$S2": "string",
155
+ "@default": { token: "@rematch", next: "@popall" }
156
+ }
157
+ }
158
+ ]
159
+ ],
160
+ whitespace: [[/[ \t\r\n]+/, "white"]],
161
+ // Only line comments
162
+ comment: [[/#.*$/, "comment"]],
163
+ // Start Flow Collections
164
+ flowCollections: [
165
+ [/\[/, "@brackets", "@array"],
166
+ [/\{/, "@brackets", "@object"]
167
+ ],
168
+ // Start Flow Scalars (quoted strings)
169
+ flowScalars: [
170
+ [/"([^"\\]|\\.)*$/, "string.invalid"],
171
+ [/'([^'\\]|\\.)*$/, "string.invalid"],
172
+ [/'[^']*'/, "string"],
173
+ [/"/, "string", "@doubleQuotedString"]
174
+ ],
175
+ doubleQuotedString: [
176
+ [/[^\\"]+/, "string"],
177
+ [/@escapes/, "string.escape"],
178
+ [/\\./, "string.escape.invalid"],
179
+ [/"/, "string", "@pop"]
180
+ ],
181
+ // Start Block Scalar
182
+ blockStyle: [[/[>|][0-9]*[+-]?$/, "operators", "@multiString"]],
183
+ // Numbers in Flow Collections (terminate with ,]})
184
+ flowNumber: [
185
+ [/@numberInteger(?=[ \t]*[,\]\}])/, "number"],
186
+ [/@numberFloat(?=[ \t]*[,\]\}])/, "number.float"],
187
+ [/@numberOctal(?=[ \t]*[,\]\}])/, "number.octal"],
188
+ [/@numberHex(?=[ \t]*[,\]\}])/, "number.hex"],
189
+ [/@numberInfinity(?=[ \t]*[,\]\}])/, "number.infinity"],
190
+ [/@numberNaN(?=[ \t]*[,\]\}])/, "number.nan"],
191
+ [/@numberDate(?=[ \t]*[,\]\}])/, "number.date"]
192
+ ],
193
+ tagHandle: [[/\![^ ]*/, "tag"]],
194
+ anchor: [[/[&*][^ ]+/, "namespace"]]
195
+ }
196
+ };
197
+ export {
198
+ conf,
199
+ language
200
+ };
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>ctlsurf-worker</title>
7
+ <script type="module" crossorigin src="./assets/index-pZmE1QXB.js"></script>
8
+ <link rel="stylesheet" crossorigin href="./assets/index-CJ6RsQWP.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@phenx-inc/ctlsurf",
3
+ "version": "0.1.0",
4
+ "description": "Agent-agnostic terminal and desktop app for ctlsurf — run Claude Code, Codex, or any coding agent with live session logging and remote control",
5
+ "main": "out/main/index.js",
6
+ "bin": {
7
+ "ctlsurf": "./bin/ctlsurf-worker.js",
8
+ "ctlsurf-worker": "./bin/ctlsurf-worker.js"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "out/",
13
+ "resources/",
14
+ "src/",
15
+ "package.json",
16
+ "electron-vite.config.ts",
17
+ "tsconfig*.json"
18
+ ],
19
+ "scripts": {
20
+ "dev": "electron-vite dev",
21
+ "dev:terminal": "esbuild src/main/headless.ts --bundle --platform=node --target=node18 --format=esm --outfile=out/headless/index.mjs --external:node-pty --sourcemap && node out/headless/index.mjs",
22
+ "build": "electron-vite build && npm run build:headless",
23
+ "build:headless": "esbuild src/main/headless.ts --bundle --platform=node --target=node18 --format=esm --outfile=out/headless/index.mjs --external:node-pty --sourcemap",
24
+ "prepublishOnly": "npm run build",
25
+ "preview": "electron-vite preview",
26
+ "package": "electron-builder",
27
+ "postinstall": "node-gyp rebuild --directory=node_modules/node-pty 2>/dev/null || true"
28
+ },
29
+ "keywords": [
30
+ "ctlsurf",
31
+ "terminal",
32
+ "tui",
33
+ "coding-agent",
34
+ "claude-code",
35
+ "codex",
36
+ "electron",
37
+ "node-pty",
38
+ "xterm"
39
+ ],
40
+ "license": "MIT",
41
+ "engines": {
42
+ "node": ">=18"
43
+ },
44
+ "dependencies": {
45
+ "@monaco-editor/react": "^4.7.0",
46
+ "@xterm/addon-fit": "^0.10.0",
47
+ "@xterm/addon-web-links": "^0.11.0",
48
+ "@xterm/xterm": "^5.5.0",
49
+ "electron-store": "^10.0.0",
50
+ "esbuild": "^0.27.4",
51
+ "monaco-editor": "^0.55.1",
52
+ "node-pty": "^1.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "@electron/rebuild": "^4.0.3",
56
+ "@types/node": "^22.15.0",
57
+ "@types/react": "^19.1.0",
58
+ "@types/react-dom": "^19.1.0",
59
+ "@vitejs/plugin-react": "^4.5.2",
60
+ "electron": "^35.0.0",
61
+ "electron-builder": "^25.1.8",
62
+ "electron-vite": "^3.1.0",
63
+ "react": "^19.1.0",
64
+ "react-dom": "^19.1.0",
65
+ "typescript": "^5.8.3"
66
+ }
67
+ }
Binary file
Binary file
Binary file
@@ -0,0 +1,46 @@
1
+ export interface AgentConfig {
2
+ id: string
3
+ name: string
4
+ command: string
5
+ args: string[]
6
+ description: string
7
+ }
8
+
9
+ function getShellCommand(): string {
10
+ if (process.platform === 'win32') return 'powershell.exe'
11
+ return process.env.SHELL || '/bin/zsh'
12
+ }
13
+
14
+ export function getBuiltinAgents(): AgentConfig[] {
15
+ return [
16
+ {
17
+ id: 'shell',
18
+ name: 'Shell',
19
+ command: getShellCommand(),
20
+ args: ['-l'], // login shell to load PATH
21
+ description: 'Default system shell'
22
+ },
23
+ {
24
+ id: 'claude',
25
+ name: 'Claude Code',
26
+ command: 'claude',
27
+ args: [],
28
+ description: 'Anthropic Claude Code CLI'
29
+ },
30
+ {
31
+ id: 'codex',
32
+ name: 'Codex CLI',
33
+ command: 'codex',
34
+ args: [],
35
+ description: 'OpenAI Codex CLI'
36
+ }
37
+ ]
38
+ }
39
+
40
+ export function getDefaultAgent(): AgentConfig {
41
+ return getBuiltinAgents()[0]
42
+ }
43
+
44
+ export function isCodingAgent(agent: AgentConfig): boolean {
45
+ return agent.id !== 'shell'
46
+ }
@@ -0,0 +1,180 @@
1
+ import { CtlsurfApi } from './ctlsurfApi'
2
+
3
+ /**
4
+ * Conversation Bridge
5
+ *
6
+ * Taps the pty output stream and logs chunks to a ctlsurf log block.
7
+ * Generic approach: buffers terminal output and flushes periodically.
8
+ */
9
+ export class ConversationBridge {
10
+ private api: CtlsurfApi
11
+ private logBlockId: string | null = null
12
+ private pageId: string | null = null
13
+ private buffer: string = ''
14
+ private flushTimer: ReturnType<typeof setTimeout> | null = null
15
+ private flushIntervalMs: number = 3000 // flush every 3 seconds
16
+ private agentName: string = 'shell'
17
+ private sessionActive: boolean = false
18
+ private inputBuffer: string = ''
19
+
20
+ constructor(api: CtlsurfApi) {
21
+ this.api = api
22
+ }
23
+
24
+ /**
25
+ * Start a new logging session.
26
+ * Creates a log block on the given dataspace page.
27
+ */
28
+ async startSession(dataspacePageId: string, agentName: string, cwd: string): Promise<void> {
29
+ if (!this.api.getApiKey()) {
30
+ console.log('[bridge] No API key set, skipping session logging')
31
+ return
32
+ }
33
+
34
+ this.pageId = dataspacePageId
35
+ this.agentName = agentName
36
+ this.buffer = ''
37
+ this.inputBuffer = ''
38
+
39
+ try {
40
+ const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19)
41
+ const block = await this.api.createBlock(dataspacePageId, {
42
+ type: 'log',
43
+ title: `${agentName} — ${timestamp} — ${cwd}`,
44
+ props: {
45
+ entries: [],
46
+ max_entries: 1000
47
+ }
48
+ })
49
+ this.logBlockId = block.id
50
+ this.sessionActive = true
51
+
52
+ // Log session start
53
+ await this.api.appendLog(this.logBlockId, 'session_start', `Started ${agentName} session`, {
54
+ agent: agentName,
55
+ cwd,
56
+ timestamp
57
+ })
58
+
59
+ console.log(`[bridge] Session started, log block: ${this.logBlockId}`)
60
+ } catch (err: any) {
61
+ console.error(`[bridge] Failed to start session:`, err.message)
62
+ this.sessionActive = false
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Feed terminal output data into the bridge.
68
+ * Buffers and flushes periodically.
69
+ */
70
+ feedOutput(data: string): void {
71
+ if (!this.sessionActive) return
72
+
73
+ this.buffer += data
74
+
75
+ // Reset flush timer
76
+ if (this.flushTimer) {
77
+ clearTimeout(this.flushTimer)
78
+ }
79
+ this.flushTimer = setTimeout(() => this.flush(), this.flushIntervalMs)
80
+ }
81
+
82
+ /**
83
+ * Feed user input data into the bridge.
84
+ */
85
+ feedInput(data: string): void {
86
+ if (!this.sessionActive) return
87
+ this.inputBuffer += data
88
+
89
+ // Detect Enter key (newline) — flush the input as a user prompt
90
+ if (data.includes('\r') || data.includes('\n')) {
91
+ const input = this.inputBuffer.trim()
92
+ if (input.length > 0) {
93
+ this.logEntry('user_input', input)
94
+ }
95
+ this.inputBuffer = ''
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Flush buffered output to ctlsurf.
101
+ */
102
+ private async flush(): Promise<void> {
103
+ if (!this.logBlockId || this.buffer.length === 0) return
104
+
105
+ const chunk = this.buffer
106
+ this.buffer = ''
107
+
108
+ // Strip ANSI escape codes for cleaner log entries
109
+ const cleaned = stripAnsi(chunk)
110
+ if (cleaned.trim().length === 0) return
111
+
112
+ try {
113
+ await this.api.appendLog(this.logBlockId, 'terminal_output', cleaned)
114
+ } catch (err: any) {
115
+ console.error(`[bridge] Failed to append log:`, err.message)
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Log a specific entry immediately.
121
+ */
122
+ private async logEntry(action: string, message: string, data?: Record<string, unknown>): Promise<void> {
123
+ if (!this.logBlockId) return
124
+ try {
125
+ await this.api.appendLog(this.logBlockId, action, message, data)
126
+ } catch (err: any) {
127
+ console.error(`[bridge] Failed to log entry:`, err.message)
128
+ }
129
+ }
130
+
131
+ /**
132
+ * End the current session.
133
+ */
134
+ async endSession(exitCode?: number): Promise<void> {
135
+ if (!this.sessionActive || !this.logBlockId) return
136
+
137
+ // Flush remaining buffer
138
+ await this.flush()
139
+
140
+ try {
141
+ await this.api.appendLog(this.logBlockId, 'session_end', `Session ended (exit code: ${exitCode ?? 'unknown'})`, {
142
+ agent: this.agentName,
143
+ exitCode
144
+ })
145
+ } catch (err: any) {
146
+ console.error(`[bridge] Failed to log session end:`, err.message)
147
+ }
148
+
149
+ if (this.flushTimer) {
150
+ clearTimeout(this.flushTimer)
151
+ this.flushTimer = null
152
+ }
153
+
154
+ this.sessionActive = false
155
+ this.logBlockId = null
156
+ this.buffer = ''
157
+ this.inputBuffer = ''
158
+ console.log('[bridge] Session ended')
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Strip ANSI escape codes from terminal output.
164
+ */
165
+ function stripAnsi(str: string): string {
166
+ return str
167
+ // CSI sequences (e.g. \x1b[0m, \x1b[?2004h, \x1b[1;32m)
168
+ .replace(/\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g, '')
169
+ // OSC sequences (e.g. \x1b]0;title\x07)
170
+ .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
171
+ // Other escape sequences (charset, keypad mode, etc.)
172
+ .replace(/\x1b[^[\]](.|$)/g, '')
173
+ // Remaining single ESC
174
+ .replace(/\x1b/g, '')
175
+ // Carriage returns
176
+ .replace(/\r/g, '')
177
+ // Control characters except newline/tab
178
+ // eslint-disable-next-line no-control-regex
179
+ .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
180
+ }
@@ -0,0 +1,142 @@
1
+ const CTLSURF_BASE_URL = 'https://app.ctlsurf.com/api'
2
+
3
+ export class CtlsurfApi {
4
+ private baseUrl: string
5
+ private apiKey: string | null = null
6
+
7
+ constructor(baseUrl?: string) {
8
+ this.baseUrl = baseUrl || CTLSURF_BASE_URL
9
+ }
10
+
11
+ setApiKey(key: string): void {
12
+ this.apiKey = key
13
+ }
14
+
15
+ setBaseUrl(url: string): void {
16
+ this.baseUrl = url.endsWith('/api') ? url : `${url}/api`
17
+ }
18
+
19
+ getApiKey(): string | null {
20
+ return this.apiKey
21
+ }
22
+
23
+ private headers(): Record<string, string> {
24
+ const h: Record<string, string> = { 'Content-Type': 'application/json' }
25
+ if (this.apiKey) {
26
+ h['Authorization'] = `Bearer ${this.apiKey}`
27
+ }
28
+ return h
29
+ }
30
+
31
+ private async request(method: string, path: string, body?: unknown): Promise<any> {
32
+ const url = `${this.baseUrl}${path}`
33
+ const opts: RequestInit = {
34
+ method,
35
+ headers: this.headers()
36
+ }
37
+ if (body) {
38
+ opts.body = JSON.stringify(body)
39
+ }
40
+
41
+ const res = await fetch(url, opts)
42
+ if (!res.ok) {
43
+ const text = await res.text()
44
+ throw new Error(`ctlsurf API ${method} ${path}: ${res.status} ${text}`)
45
+ }
46
+ return res.json()
47
+ }
48
+
49
+ // ─── Pages ───────────────────────────────────────────
50
+
51
+ async createPage(params: {
52
+ title: string
53
+ type?: string
54
+ parent_id?: string
55
+ folder_id?: string
56
+ cwd?: string
57
+ tags?: string[]
58
+ }): Promise<any> {
59
+ return this.request('POST', '/pages', params)
60
+ }
61
+
62
+ async findPageByRootPath(rootPath: string): Promise<any> {
63
+ return this.request('POST', '/pages/find-by-root-path', { root_path: rootPath })
64
+ }
65
+
66
+ // ─── Blocks ──────────────────────────────────────────
67
+
68
+ async createBlock(pageId: string, params: {
69
+ type: string
70
+ title?: string
71
+ props?: Record<string, unknown>
72
+ }): Promise<any> {
73
+ return this.request('POST', `/blocks/page/${pageId}`, params)
74
+ }
75
+
76
+ async getBlock(blockId: string): Promise<any> {
77
+ return this.request('GET', `/blocks/${blockId}`)
78
+ }
79
+
80
+ async updateBlock(blockId: string, params: {
81
+ props?: Record<string, unknown>
82
+ title?: string
83
+ }): Promise<any> {
84
+ return this.request('PUT', `/blocks/${blockId}`, params)
85
+ }
86
+
87
+ // ─── Folders ────────────────────────────────────────
88
+
89
+ async createFolder(params: { name: string; root_path: string }): Promise<any> {
90
+ return this.request('POST', '/folders', params)
91
+ }
92
+
93
+ // ─── Workers ────────────────────────────────────────
94
+
95
+ async getAuthCode(): Promise<{ code: string }> {
96
+ return this.request('POST', '/workers/token-exchange')
97
+ }
98
+
99
+ async findFolderByPath(rootPath: string): Promise<any> {
100
+ return this.request('POST', '/folders/find-by-path', { root_path: rootPath })
101
+ }
102
+
103
+ async getFolderPages(folderId: string): Promise<any[]> {
104
+ const folder = await this.request('GET', `/folders/${folderId}`)
105
+ return folder?.pages || []
106
+ }
107
+
108
+ async findFolderByGitRemote(gitRemote: string): Promise<any> {
109
+ // Search folders by listing all and matching git_remote
110
+ const folders = await this.request('GET', '/folders')
111
+ return folders?.find((f: any) => f.git_remote === gitRemote || f.root_path === gitRemote) || null
112
+ }
113
+
114
+ // ─── Log convenience ─────────────────────────────────
115
+
116
+ async appendLog(blockId: string, action: string, message: string, data?: Record<string, unknown>): Promise<any> {
117
+ // Read-modify-write: get current entries, append, put back
118
+ const block = await this.getBlock(blockId)
119
+ const props = block.props || {}
120
+ const entries = Array.isArray(props.entries) ? [...props.entries] : []
121
+ const maxEntries = props.max_entries || 1000
122
+
123
+ const entry: Record<string, unknown> = {
124
+ _id: `log_${entries.length}`,
125
+ _timestamp: new Date().toISOString(),
126
+ action,
127
+ message
128
+ }
129
+ if (data) {
130
+ entry.data = data
131
+ }
132
+
133
+ entries.push(entry)
134
+
135
+ // Trim oldest if over max
136
+ const trimmed = entries.length > maxEntries ? entries.slice(-maxEntries) : entries
137
+
138
+ return this.updateBlock(blockId, {
139
+ props: { ...props, entries: trimmed }
140
+ })
141
+ }
142
+ }
@@ -0,0 +1,17 @@
1
+ export function detectMode(argv: string[]): 'gui' | 'terminal' {
2
+ if (argv.includes('--terminal')) return 'terminal'
3
+ if (argv.includes('--desktop')) return 'gui'
4
+
5
+ // SSH session → terminal
6
+ if (process.env.SSH_CONNECTION || process.env.SSH_TTY) return 'terminal'
7
+
8
+ // Docker / container → terminal
9
+ if (process.env.container || process.env.DOCKER_CONTAINER) return 'terminal'
10
+
11
+ // Linux: no display → terminal
12
+ if (process.platform === 'linux') {
13
+ if (!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return 'terminal'
14
+ }
15
+
16
+ return 'gui'
17
+ }