git.iko.soy/rowtype-yoga/purescript-claude-code
on main help sign in
Bind Claude mod registration, native images, and private transports
9e85fd2Mark
LICENSEnew file+21
unifiedsplit
@@ -0,0 +1,21 @@
1+MIT License
2+
3+Copyright (c) 2026 Mark Eibes
4+
5+Permission is hereby granted, free of charge, to any person obtaining a copy
6+of this software and associated documentation files (the "Software"), to deal
7+in the Software without restriction, including without limitation the rights
8+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+copies of the Software, and to permit persons to whom the Software is
10+furnished to do so, subject to the following conditions:
11+
12+The above copyright notice and this permission notice shall be included in all
13+copies or substantial portions of the Software.
14+
15+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+SOFTWARE.
package.jsonnew file+8
@@ -0,0 +1,8 @@
1+{
2+ "name": "purescript-claude-code",
3+ "description": "PureScript bindings for Claude Code plugins",
4+ "license": "MIT",
5+ "repository": "github:rowtype-yoga/purescript-claude-code",
6+ "private": true,
7+ "type": "module"
8+}
spago.yamlnew file+11
@@ -0,0 +1,11 @@
1+package:
2+ name: claude-code
3+ dependencies:
4+ - effect: ">=4.0.0 <5.0.0"
5+ - foreign: ">=7.0.0 <8.0.0"
6+ - js-promise: ">=1.0.0 <2.0.0"
7+ - nullable: ">=6.0.0 <7.0.0"
8+ - prelude: ">=6.0.2 <7.0.0"
9+workspace:
10+ packageSet:
11+ registry: 73.2.0
src/ClaudeCode.jsnew file+143
@@ -0,0 +1,143 @@
1+const omitEmpty = object =>
2+ Object.fromEntries(Object.entries(object).filter(([, value]) => value !== ""));
3+
4+export const capabilitiesName = capabilities => capabilities.pluginName;
5+export const capabilitiesRoot = capabilities => capabilities.pluginRoot;
6+
7+export const next = continuation => event => () => continuation(event);
8+
9+export const registerCommand = capabilities => command => () =>
10+ capabilities.commandRegister({
11+ name: command.name,
12+ description: command.description,
13+ argumentHint: command.argumentHint || undefined,
14+ });
15+
16+export const registerTool = capabilities => tool => () =>
17+ capabilities.toolRegister({
18+ name: tool.name,
19+ description: tool.description,
20+ inputSchema: tool.inputSchema,
21+ isDeferred: false,
22+ }).then(result => result.tool);
23+
24+export const spawn = capabilities => request => onChunk => onResult => () => {
25+ const stream = capabilities.processSpawn({ argv: request.argv, cwd: request.cwd });
26+ const state = { stream, cancelled: false };
27+ state.done = (async () => {
28+ try {
29+ let next = await stream.next();
30+ while (!next.done) {
31+ if (!state.cancelled) onChunk({ stream: next.value.stream, text: next.value.text })();
32+ next = await stream.next();
33+ }
34+ const value = next.value ?? { code: null, signal: null };
35+ if (!state.cancelled) onResult({ code: value.code, signal: value.signal })();
36+ return { code: value.code, signal: value.signal };
37+ } catch (error) {
38+ const value = {
39+ code: null,
40+ signal: error instanceof Error ? error.message : String(error),
41+ };
42+ if (!state.cancelled) onResult(value)();
43+ return value;
44+ }
45+ })();
46+ return state;
47+};
48+
49+export const awaitSpawn = spawn => () => spawn.done;
50+
51+export const cancelSpawn = spawn => () => {
52+ if (spawn.cancelled) return;
53+ spawn.cancelled = true;
54+ void spawn.stream.return?.();
55+};
56+
57+export const after = capabilities => milliseconds => callback => () =>
58+ capabilities.clockAfter(milliseconds, callback);
59+
60+export const cancelTimer = timer => () => timer.cancel();
61+
62+export const fetch = capabilities => request => () =>
63+ capabilities.httpFetch(request.url, {
64+ method: request.method,
65+ headers: { Authorization: request.authorization },
66+ body: request.body == null ? undefined : request.body,
67+ }).then(response => ({ ok: response.ok, status: response.status, text: response.text }));
68+
69+export const blit = capabilities => request => () =>
70+ capabilities.uiBlit({
71+ requestId: request.requestId,
72+ key: request.key,
73+ source: request.source,
74+ columns: request.columns,
75+ rows: request.rows,
76+ }).then(result => result.deny ?? null);
77+
78+export const invalidateRender = capabilities => () => capabilities.uiInvalidate();
79+
80+export const configTheme = capabilities => () =>
81+ capabilities.configList().then(rows => {
82+ const theme = rows.find(row => row.key === "theme");
83+ return { theme: typeof theme?.value === "string" ? theme.value : null };
84+ });
85+
86+export const commandInput = event => {
87+ if (event == null || typeof event.command !== "string" || typeof event.args !== "string") return null;
88+ return { command: event.command, args: event.args };
89+};
90+
91+export const toolCall = event => {
92+ if (event == null || typeof event.tool !== "string") return null;
93+ return { tool: event.tool, input: event };
94+};
95+
96+export const sessionStart = event =>
97+ event != null && typeof event.cwd === "string" ? { cwd: event.cwd } : null;
98+
99+export const configSet = event => {
100+ if (event == null || typeof event.key !== "string") return null;
101+ return { key: event.key, value: typeof event.value === "string" ? event.value : null };
102+};
103+
104+export const renderContext = capabilities => event => () => {
105+ if (
106+ event == null ||
107+ typeof event.requestId !== "string" ||
108+ typeof event.surface !== "string" ||
109+ typeof event.component !== "string"
110+ ) return null;
111+ const props = event.props ?? {};
112+ return {
113+ elements: capabilities.uiResolve(event),
114+ input: {
115+ requestId: event.requestId,
116+ surface: event.surface,
117+ component: event.component,
118+ bodyColumns: Number.isInteger(props.bodyColumns) ? props.bodyColumns : 0,
119+ maxRows: Number.isInteger(props.maxRows) ? props.maxRows : 0,
120+ hasSurvey: props.hasSurvey === true,
121+ },
122+ };
123+};
124+
125+export const renderInput = context => context.input;
126+
127+export const box = context => props => children =>
128+ context.elements.Box({ ...omitEmpty(props), children });
129+
130+export const text = context => props => value =>
131+ context.elements.Text({ ...props, children: value });
132+
133+export const button = context => props => onPress =>
134+ context.elements.Button({
135+ ...omitEmpty(props),
136+ plain: props.plain ? true : undefined,
137+ autoFocus: props.autoFocus ? true : undefined,
138+ onPress: () => onPress(),
139+ });
140+
141+export const image = context => props => context.elements.Image(props);
142+
143+export const renderElement = element => element;
src/ClaudeCode.pursnew file+213
@@ -0,0 +1,213 @@
1+module ClaudeCode
2+ ( Capabilities
3+ , Spawn
4+ , Timer
5+ , RenderContext
6+ , Element
7+ , ProcessChunk
8+ , ProcessResult
9+ , HttpResponse
10+ , RenderInput
11+ , CommandInput
12+ , ToolCall
13+ , SessionStart
14+ , ConfigSet
15+ , ThemeConfig
16+ , CommandSpec
17+ , ToolSpec
18+ , ImageSource
19+ , ImageProps
20+ , BlitRequest
21+ , BoxProps
22+ , TextProps
23+ , ButtonProps
24+ , capabilitiesName
25+ , capabilitiesRoot
26+ , next
27+ , registerCommand
28+ , registerTool
29+ , spawn
30+ , awaitSpawn
31+ , cancelSpawn
32+ , after
33+ , cancelTimer
34+ , fetch
35+ , blit
36+ , invalidateRender
37+ , configTheme
38+ , commandInput
39+ , toolCall
40+ , sessionStart
41+ , configSet
42+ , renderContext
43+ , renderInput
44+ , renderElement
45+ , box
46+ , text
47+ , button
48+ , image
49+ ) where
50+
51+import Prelude
52+
53+import Data.Nullable (Nullable)
54+import Effect (Effect)
55+import Foreign (Foreign)
56+import Promise (Promise)
57+
58+type ProcessChunk =
59+ { stream :: String
60+ , text :: String
61+ }
62+
63+type ProcessResult =
64+ { code :: Nullable Int
65+ , signal :: Nullable String
66+ }
67+
68+type HttpResponse =
69+ { ok :: Boolean
70+ , status :: Int
71+ , text :: String
72+ }
73+
74+type RenderInput =
75+ { requestId :: String
76+ , surface :: String
77+ , component :: String
78+ , bodyColumns :: Int
79+ , maxRows :: Int
80+ , hasSurvey :: Boolean
81+ }
82+
83+type CommandInput =
84+ { command :: String
85+ , args :: String
86+ }
87+
88+type ToolCall =
89+ { tool :: String
90+ , input :: Foreign
91+ }
92+
93+type SessionStart =
94+ { cwd :: String }
95+
96+type ConfigSet =
97+ { key :: String
98+ , value :: Nullable String
99+ }
100+
101+type ThemeConfig =
102+ { theme :: Nullable String }
103+
104+type CommandSpec =
105+ { name :: String
106+ , description :: String
107+ , argumentHint :: String
108+ }
109+
110+type ToolSpec =
111+ { name :: String
112+ , description :: String
113+ , inputSchema :: Foreign
114+ }
115+
116+type ImageSource =
117+ { shm :: String
118+ , format :: String
119+ , width :: Int
120+ , height :: Int
121+ }
122+
123+type ImageProps =
124+ { key :: String
125+ , source :: ImageSource
126+ , columns :: Int
127+ , rows :: Int
128+ , alt :: String
129+ }
130+
131+type BlitRequest =
132+ { requestId :: String
133+ , key :: String
134+ , source :: ImageSource
135+ , columns :: Int
136+ , rows :: Int
137+ }
138+
139+type BoxProps =
140+ { flexDirection :: String
141+ , borderStyle :: String
142+ , borderColor :: String
143+ , paddingX :: Int
144+ , columnGap :: Int
145+ }
146+
147+type TextProps =
148+ { color :: String
149+ , dimColor :: Boolean
150+ }
151+
152+type ButtonProps =
153+ { key :: String
154+ , label :: String
155+ , hotkey :: String
156+ , plain :: Boolean
157+ , variant :: String
158+ , role :: String
159+ , autoFocus :: Boolean
160+ }
161+
162+foreign import data Capabilities :: Type
163+foreign import data Spawn :: Type
164+foreign import data Timer :: Type
165+foreign import data RenderContext :: Type
166+foreign import data Element :: Type
167+
168+foreign import capabilitiesName :: Capabilities -> String
169+foreign import capabilitiesRoot :: Capabilities -> String
170+
171+-- | Continues an event chain without exposing host callback representation.
172+foreign import next :: Foreign -> Foreign -> Effect (Promise Foreign)
173+
174+foreign import registerCommand :: Capabilities -> CommandSpec -> Effect (Promise Unit)
175+foreign import registerTool :: Capabilities -> ToolSpec -> Effect (Promise String)
176+
177+-- | Drains a spawned host process until it exits. Cancelling this stream invokes
178+-- | AsyncGenerator.return, which is the host-supported process termination path.
179+foreign import spawn
180+ :: Capabilities
181+ -> { argv :: Array String, cwd :: String }
182+ -> (ProcessChunk -> Effect Unit)
183+ -> (ProcessResult -> Effect Unit)
184+ -> Effect Spawn
185+
186+foreign import awaitSpawn :: Spawn -> Effect (Promise ProcessResult)
187+foreign import cancelSpawn :: Spawn -> Effect Unit
188+foreign import after :: Capabilities -> Int -> Effect Unit -> Effect Timer
189+foreign import cancelTimer :: Timer -> Effect Unit
190+-- | Calls the authenticated metadata bridge. The binding deliberately carries
191+-- | text only; shared-memory pixels never enter the plugin VM.
192+foreign import fetch
193+ :: Capabilities
194+ -> { url :: String, method :: String, authorization :: String, body :: Nullable String }
195+ -> Effect (Promise HttpResponse)
196+
197+-- | A denial is the host's explicit reason; an empty nullable value means the
198+-- | mounted Image accepted the source. Acceptance is not terminal consumption.
199+foreign import blit :: Capabilities -> BlitRequest -> Effect (Promise (Nullable String))
200+foreign import invalidateRender :: Capabilities -> Effect Unit
201+foreign import configTheme :: Capabilities -> Effect (Promise ThemeConfig)
202+foreign import commandInput :: Foreign -> Nullable CommandInput
203+foreign import toolCall :: Foreign -> Nullable ToolCall
204+foreign import sessionStart :: Foreign -> Nullable SessionStart
205+foreign import configSet :: Foreign -> Nullable ConfigSet
206+
207+foreign import renderContext :: Capabilities -> Foreign -> Effect (Nullable RenderContext)
208+foreign import renderInput :: RenderContext -> RenderInput
209+foreign import renderElement :: Element -> Foreign
210+foreign import box :: RenderContext -> BoxProps -> Array Element -> Element
211+foreign import text :: RenderContext -> TextProps -> String -> Element
212+foreign import button :: RenderContext -> ButtonProps -> Effect Unit -> Element
213+foreign import image :: RenderContext -> ImageProps -> Element