@react-grab/gemini 0.0.87 → 0.0.89

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.
package/dist/cli.cjs CHANGED
@@ -7138,7 +7138,7 @@ var import_picocolors = __toESM(require_picocolors());
7138
7138
  var DEFAULT_PORT = 8567;
7139
7139
 
7140
7140
  // src/cli.ts
7141
- var VERSION = "0.0.87";
7141
+ var VERSION = "0.0.89";
7142
7142
  var serverPath = path2.join(__dirname, "server.cjs");
7143
7143
  execa(process.execPath, [serverPath], {
7144
7144
  detached: true,
package/dist/cli.js CHANGED
@@ -7130,7 +7130,7 @@ var import_picocolors = __toESM(require_picocolors());
7130
7130
  var DEFAULT_PORT = 8567;
7131
7131
 
7132
7132
  // src/cli.ts
7133
- var VERSION = "0.0.87";
7133
+ var VERSION = "0.0.89";
7134
7134
  var serverPath = join(__dirname, "server.cjs");
7135
7135
  execa(process.execPath, [serverPath], {
7136
7136
  detached: true,
package/dist/client.cjs CHANGED
@@ -12,7 +12,7 @@ var parseSSEEvent = (eventBlock) => {
12
12
  }
13
13
  return { eventType, data };
14
14
  };
15
- async function* streamSSE(stream, signal) {
15
+ var streamSSE = async function* (stream, signal) {
16
16
  const reader = stream.getReader();
17
17
  const decoder = new TextDecoder();
18
18
  let buffer = "";
@@ -51,28 +51,54 @@ async function* streamSSE(stream, signal) {
51
51
  } catch {
52
52
  }
53
53
  }
54
- }
55
-
56
- // src/constants.ts
57
- var DEFAULT_PORT = 8567;
58
- var COMPLETED_STATUS = "Completed successfully";
59
-
60
- // src/client.ts
61
- var DEFAULT_SERVER_URL = `http://localhost:${DEFAULT_PORT}`;
62
- async function* streamFromServer(serverUrl, context, signal) {
54
+ };
55
+ var isRecord = (value) => typeof value === "object" && value !== null;
56
+ var getStoredAgentContext = (storage, sessionId, storageKey = STORAGE_KEY) => {
57
+ const rawSessions = storage.getItem(storageKey);
58
+ if (!rawSessions) throw new Error("No sessions to resume");
59
+ let parsed;
60
+ try {
61
+ parsed = JSON.parse(rawSessions);
62
+ } catch {
63
+ throw new Error("Failed to parse stored sessions");
64
+ }
65
+ if (!isRecord(parsed)) throw new Error("Invalid stored sessions");
66
+ const storedSession = parsed[sessionId];
67
+ if (!isRecord(storedSession)) {
68
+ throw new Error(`Session ${sessionId} not found`);
69
+ }
70
+ const context = storedSession.context;
71
+ if (!isRecord(context)) throw new Error(`Session ${sessionId} is invalid`);
72
+ const content = context.content;
73
+ const prompt = context.prompt;
74
+ if (typeof content !== "string" || typeof prompt !== "string") {
75
+ throw new Error(`Session ${sessionId} is invalid`);
76
+ }
77
+ const options = context.options;
78
+ const storedSessionId = context.sessionId;
79
+ return {
80
+ content,
81
+ prompt,
82
+ options,
83
+ sessionId: typeof storedSessionId === "string" ? storedSessionId : void 0
84
+ };
85
+ };
86
+ var streamAgentStatusFromServer = async function* (options, context, signal) {
63
87
  const startTime = Date.now();
64
88
  const sessionId = context.sessionId;
89
+ const pollIntervalMs = options.pollIntervalMs ?? 100;
90
+ const agentUrl = `${options.serverUrl}${options.agentPath ?? "/agent"}`;
65
91
  const handleAbort = () => {
66
- if (sessionId) {
67
- fetch(`${serverUrl}/abort/${sessionId}`, { method: "POST" }).catch(
68
- () => {
69
- }
70
- );
71
- }
92
+ if (!sessionId) return;
93
+ const abortPath = options.abortPath?.(sessionId) ?? `/abort/${sessionId}`;
94
+ fetch(`${options.serverUrl}${abortPath}`, { method: "POST" }).catch(
95
+ () => {
96
+ }
97
+ );
72
98
  };
73
99
  signal.addEventListener("abort", handleAbort);
74
100
  try {
75
- const response = await fetch(`${serverUrl}/agent`, {
101
+ const response = await fetch(agentUrl, {
76
102
  method: "POST",
77
103
  headers: { "Content-Type": "application/json" },
78
104
  body: JSON.stringify(context),
@@ -85,29 +111,29 @@ async function* streamFromServer(serverUrl, context, signal) {
85
111
  throw new Error("No response body");
86
112
  }
87
113
  const iterator = streamSSE(response.body, signal)[Symbol.asyncIterator]();
88
- let done = false;
114
+ let isDone = false;
89
115
  let pendingNext = iterator.next();
90
116
  let lastStatus = null;
91
- while (!done) {
117
+ while (!isDone) {
92
118
  const result = await Promise.race([
93
119
  pendingNext.then((iteratorResult) => ({
94
120
  type: "status",
95
121
  iteratorResult
96
122
  })),
97
123
  new Promise(
98
- (resolve) => setTimeout(() => resolve({ type: "timeout" }), 100)
124
+ (resolve) => setTimeout(() => resolve({ type: "timeout" }), pollIntervalMs)
99
125
  )
100
126
  ]);
101
127
  const elapsedSeconds = (Date.now() - startTime) / 1e3;
102
128
  if (result.type === "status") {
103
129
  const iteratorResult = result.iteratorResult;
104
- done = iteratorResult.done ?? false;
105
- if (!done && iteratorResult.value) {
130
+ isDone = iteratorResult.done ?? false;
131
+ if (!isDone && iteratorResult.value) {
106
132
  lastStatus = iteratorResult.value;
107
133
  pendingNext = iterator.next();
108
134
  }
109
135
  }
110
- if (lastStatus === COMPLETED_STATUS) {
136
+ if (lastStatus === options.completedStatus) {
111
137
  yield `Completed in ${elapsedSeconds.toFixed(1)}s`;
112
138
  } else if (lastStatus) {
113
139
  yield `${lastStatus} ${elapsedSeconds.toFixed(1)}s`;
@@ -118,57 +144,74 @@ async function* streamFromServer(serverUrl, context, signal) {
118
144
  } finally {
119
145
  signal.removeEventListener("abort", handleAbort);
120
146
  }
121
- }
147
+ };
148
+ var createCachedConnectionChecker = (checkConnection, ttlMs = CONNECTION_CHECK_TTL_MS) => {
149
+ let cache = null;
150
+ return async () => {
151
+ const now = Date.now();
152
+ if (cache && now - cache.timestamp < ttlMs) return cache.result;
153
+ try {
154
+ const result = await checkConnection();
155
+ cache = { result, timestamp: now };
156
+ return result;
157
+ } catch {
158
+ cache = { result: false, timestamp: now };
159
+ return false;
160
+ }
161
+ };
162
+ };
163
+
164
+ // src/constants.ts
165
+ var DEFAULT_PORT = 8567;
166
+ var COMPLETED_STATUS = "Completed successfully";
167
+
168
+ // src/client.ts
169
+ var DEFAULT_SERVER_URL = `http://localhost:${DEFAULT_PORT}`;
170
+ var isReactGrabApi = (value) => typeof value === "object" && value !== null && "setAgent" in value;
122
171
  var createGeminiAgentProvider = (providerOptions = {}) => {
123
172
  const { serverUrl = DEFAULT_SERVER_URL, getOptions } = providerOptions;
124
- let connectionCache = null;
125
173
  const mergeOptions = (contextOptions) => ({
126
174
  ...getOptions?.() ?? {},
127
175
  ...contextOptions ?? {}
128
176
  });
177
+ const checkConnection = createCachedConnectionChecker(async () => {
178
+ const response = await fetch(`${serverUrl}/health`, { method: "GET" });
179
+ return response.ok;
180
+ }, CONNECTION_CHECK_TTL_MS);
129
181
  return {
130
182
  send: async function* (context, signal) {
131
183
  const mergedContext = {
132
184
  ...context,
133
185
  options: mergeOptions(context.options)
134
186
  };
135
- yield* streamFromServer(serverUrl, mergedContext, signal);
187
+ yield* streamAgentStatusFromServer(
188
+ { serverUrl, completedStatus: COMPLETED_STATUS },
189
+ mergedContext,
190
+ signal
191
+ );
136
192
  },
137
193
  resume: async function* (sessionId, signal, storage) {
138
- const savedSessions = storage.getItem(STORAGE_KEY);
139
- if (!savedSessions) {
140
- throw new Error("No sessions to resume");
141
- }
142
- const sessionsObject = JSON.parse(savedSessions);
143
- const session = sessionsObject[sessionId];
144
- if (!session) {
145
- throw new Error(`Session ${sessionId} not found`);
146
- }
147
- const context = session.context;
194
+ const storedContext = getStoredAgentContext(storage, sessionId);
195
+ const context = {
196
+ content: storedContext.content,
197
+ prompt: storedContext.prompt,
198
+ options: storedContext.options,
199
+ sessionId: storedContext.sessionId ?? sessionId
200
+ };
148
201
  const mergedContext = {
149
202
  ...context,
150
203
  options: mergeOptions(context.options)
151
204
  };
152
205
  yield "Resuming...";
153
- yield* streamFromServer(serverUrl, mergedContext, signal);
206
+ yield* streamAgentStatusFromServer(
207
+ { serverUrl, completedStatus: COMPLETED_STATUS },
208
+ mergedContext,
209
+ signal
210
+ );
154
211
  },
155
212
  supportsResume: true,
156
213
  supportsFollowUp: true,
157
- checkConnection: async () => {
158
- const now = Date.now();
159
- if (connectionCache && now - connectionCache.timestamp < CONNECTION_CHECK_TTL_MS) {
160
- return connectionCache.result;
161
- }
162
- try {
163
- const response = await fetch(`${serverUrl}/health`, { method: "GET" });
164
- const result = response.ok;
165
- connectionCache = { result, timestamp: now };
166
- return result;
167
- } catch {
168
- connectionCache = { result: false, timestamp: now };
169
- return false;
170
- }
171
- },
214
+ checkConnection,
172
215
  abort: async (sessionId) => {
173
216
  try {
174
217
  await fetch(`${serverUrl}/abort/${sessionId}`, { method: "POST" });
@@ -186,24 +229,25 @@ var createGeminiAgentProvider = (providerOptions = {}) => {
186
229
  var attachAgent = async () => {
187
230
  if (typeof window === "undefined") return;
188
231
  const provider = createGeminiAgentProvider();
189
- const attach = (api2) => {
190
- api2.setAgent({ provider, storage: sessionStorage });
232
+ const attach = (api) => {
233
+ api.setAgent({ provider, storage: sessionStorage });
191
234
  };
192
- const api = window.__REACT_GRAB__;
193
- if (api) {
194
- attach(api);
235
+ const existingApi = window.__REACT_GRAB__;
236
+ if (isReactGrabApi(existingApi)) {
237
+ attach(existingApi);
195
238
  return;
196
239
  }
197
240
  window.addEventListener(
198
241
  "react-grab:init",
199
242
  (event) => {
200
- const customEvent = event;
201
- attach(customEvent.detail);
243
+ if (!(event instanceof CustomEvent)) return;
244
+ if (!isReactGrabApi(event.detail)) return;
245
+ attach(event.detail);
202
246
  },
203
247
  { once: true }
204
248
  );
205
249
  const apiAfterListener = window.__REACT_GRAB__;
206
- if (apiAfterListener) {
250
+ if (isReactGrabApi(apiAfterListener)) {
207
251
  attach(apiAfterListener);
208
252
  }
209
253
  };
@@ -1,5 +1,5 @@
1
- var ReactGrabGemini=(function(exports){'use strict';var f=5e3,g="react-grab:agent-sessions",w=c=>{let e="",o="";for(let n of c.split(`
2
- `))n.startsWith("event:")?e=n.slice(6).trim():n.startsWith("data:")&&(o=n.slice(5).trim());return {eventType:e,data:o}};async function*A(c,e){let o=c.getReader(),n=new TextDecoder,r="",t=false,s=()=>{t=true,o.cancel().catch(()=>{});};e.addEventListener("abort",s);try{if(e.aborted)throw new DOMException("Aborted","AbortError");for(;;){let i=await o.read();if(t||e.aborted)throw new DOMException("Aborted","AbortError");let{done:d,value:u}=i;u&&(r+=n.decode(u,{stream:!0}));let a;for(;(a=r.indexOf(`
1
+ var ReactGrabGemini=(function(exports){'use strict';var A=5e3,v="react-grab:agent-sessions",C=e=>{let t="",n="";for(let r of e.split(`
2
+ `))r.startsWith("event:")?t=r.slice(6).trim():r.startsWith("data:")&&(n=r.slice(5).trim());return {eventType:t,data:n}},T=async function*(e,t){let n=e.getReader(),r=new TextDecoder,o="",s=false,a=()=>{s=true,n.cancel().catch(()=>{});};t.addEventListener("abort",a);try{if(t.aborted)throw new DOMException("Aborted","AbortError");for(;;){let d=await n.read();if(s||t.aborted)throw new DOMException("Aborted","AbortError");let{done:i,value:l}=d;l&&(o+=r.decode(l,{stream:!0}));let c;for(;(c=o.indexOf(`
3
3
 
4
- `))!==-1;){let{eventType:p,data:l}=w(r.slice(0,a));if(r=r.slice(a+2),p==="done")return;if(p==="error")throw new Error(l||"Agent error");l&&(yield l);}if(d)break}}finally{e.removeEventListener("abort",s);try{o.releaseLock();}catch{}}}var y="Completed successfully";var b=`http://localhost:${8567}`;async function*E(c,e,o){let n=Date.now(),r=e.sessionId,t=()=>{r&&fetch(`${c}/abort/${r}`,{method:"POST"}).catch(()=>{});};o.addEventListener("abort",t);try{let s=await fetch(`${c}/agent`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e),signal:o});if(!s.ok)throw new Error(`Server error: ${s.status}`);if(!s.body)throw new Error("No response body");let i=A(s.body,o)[Symbol.asyncIterator](),d=!1,u=i.next(),a=null;for(;!d;){let p=await Promise.race([u.then(m=>({type:"status",iteratorResult:m})),new Promise(m=>setTimeout(()=>m({type:"timeout"}),100))]),l=(Date.now()-n)/1e3;if(p.type==="status"){let m=p.iteratorResult;d=m.done??!1,!d&&m.value&&(a=m.value,u=i.next());}a===y?yield `Completed in ${l.toFixed(1)}s`:a?yield `${a} ${l.toFixed(1)}s`:yield `Working\u2026 ${l.toFixed(1)}s`;}}finally{o.removeEventListener("abort",t);}}var S=(c={})=>{let{serverUrl:e=b,getOptions:o}=c,n=null,r=t=>({...o?.()??{},...t??{}});return {send:async function*(t,s){let i={...t,options:r(t.options)};yield*E(e,i,s);},resume:async function*(t,s,i){let d=i.getItem(g);if(!d)throw new Error("No sessions to resume");let a=JSON.parse(d)[t];if(!a)throw new Error(`Session ${t} not found`);let p=a.context,l={...p,options:r(p.options)};yield "Resuming...",yield*E(e,l,s);},supportsResume:true,supportsFollowUp:true,checkConnection:async()=>{let t=Date.now();if(n&&t-n.timestamp<f)return n.result;try{let i=(await fetch(`${e}/health`,{method:"GET"})).ok;return n={result:i,timestamp:t},i}catch{return n={result:false,timestamp:t},false}},abort:async t=>{try{await fetch(`${e}/abort/${t}`,{method:"POST"});}catch{}},undo:async()=>{try{await fetch(`${e}/undo`,{method:"POST"});}catch{}}}},T=async()=>{if(typeof window>"u")return;let c=S(),e=r=>{r.setAgent({provider:c,storage:sessionStorage});},o=window.__REACT_GRAB__;if(o){e(o);return}window.addEventListener("react-grab:init",r=>{e(r.detail);},{once:true});let n=window.__REACT_GRAB__;n&&e(n);};T();
5
- exports.attachAgent=T;exports.createGeminiAgentProvider=S;return exports;})({});
4
+ `))!==-1;){let{eventType:f,data:p}=C(o.slice(0,c));if(o=o.slice(c+2),f==="done")return;if(f==="error")throw new Error(p||"Agent error");p&&(yield p);}if(i)break}}finally{t.removeEventListener("abort",a);try{n.releaseLock();}catch{}}},g=e=>typeof e=="object"&&e!==null,E=(e,t,n=v)=>{let r=e.getItem(n);if(!r)throw new Error("No sessions to resume");let o;try{o=JSON.parse(r);}catch{throw new Error("Failed to parse stored sessions")}if(!g(o))throw new Error("Invalid stored sessions");let s=o[t];if(!g(s))throw new Error(`Session ${t} not found`);let a=s.context;if(!g(a))throw new Error(`Session ${t} is invalid`);let d=a.content,i=a.prompt;if(typeof d!="string"||typeof i!="string")throw new Error(`Session ${t} is invalid`);let l=a.options,c=a.sessionId;return {content:d,prompt:i,options:l,sessionId:typeof c=="string"?c:void 0}},h=async function*(e,t,n){let r=Date.now(),o=t.sessionId,s=e.pollIntervalMs??100,a=`${e.serverUrl}${e.agentPath??"/agent"}`,d=()=>{if(!o)return;let i=e.abortPath?.(o)??`/abort/${o}`;fetch(`${e.serverUrl}${i}`,{method:"POST"}).catch(()=>{});};n.addEventListener("abort",d);try{let i=await fetch(a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t),signal:n});if(!i.ok)throw new Error(`Server error: ${i.status}`);if(!i.body)throw new Error("No response body");let l=T(i.body,n)[Symbol.asyncIterator](),c=!1,f=l.next(),p=null;for(;!c;){let S=await Promise.race([f.then(u=>({type:"status",iteratorResult:u})),new Promise(u=>setTimeout(()=>u({type:"timeout"}),s))]),m=(Date.now()-r)/1e3;if(S.type==="status"){let u=S.iteratorResult;c=u.done??!1,!c&&u.value&&(p=u.value,f=l.next());}p===e.completedStatus?yield `Completed in ${m.toFixed(1)}s`:p?yield `${p} ${m.toFixed(1)}s`:yield `Working\u2026 ${m.toFixed(1)}s`;}}finally{n.removeEventListener("abort",d);}},b=(e,t=A)=>{let n=null;return async()=>{let r=Date.now();if(n&&r-n.timestamp<t)return n.result;try{let o=await e();return n={result:o,timestamp:r},o}catch{return n={result:false,timestamp:r},false}}};var y="Completed successfully";var x=`http://localhost:${8567}`,w=e=>typeof e=="object"&&e!==null&&"setAgent"in e,_=(e={})=>{let{serverUrl:t=x,getOptions:n}=e,r=s=>({...n?.()??{},...s??{}}),o=b(async()=>(await fetch(`${t}/health`,{method:"GET"})).ok,A);return {send:async function*(s,a){let d={...s,options:r(s.options)};yield*h({serverUrl:t,completedStatus:y},d,a);},resume:async function*(s,a,d){let i=E(d,s),l={content:i.content,prompt:i.prompt,options:i.options,sessionId:i.sessionId??s},c={...l,options:r(l.options)};yield "Resuming...",yield*h({serverUrl:t,completedStatus:y},c,a);},supportsResume:true,supportsFollowUp:true,checkConnection:o,abort:async s=>{try{await fetch(`${t}/abort/${s}`,{method:"POST"});}catch{}},undo:async()=>{try{await fetch(`${t}/undo`,{method:"POST"});}catch{}}}},R=async()=>{if(typeof window>"u")return;let e=_(),t=o=>{o.setAgent({provider:e,storage:sessionStorage});},n=window.__REACT_GRAB__;if(w(n)){t(n);return}window.addEventListener("react-grab:init",o=>{o instanceof CustomEvent&&w(o.detail)&&t(o.detail);},{once:true});let r=window.__REACT_GRAB__;w(r)&&t(r);};R();
5
+ exports.attachAgent=R;exports.createGeminiAgentProvider=_;return exports;})({});
package/dist/client.js CHANGED
@@ -10,7 +10,7 @@ var parseSSEEvent = (eventBlock) => {
10
10
  }
11
11
  return { eventType, data };
12
12
  };
13
- async function* streamSSE(stream, signal) {
13
+ var streamSSE = async function* (stream, signal) {
14
14
  const reader = stream.getReader();
15
15
  const decoder = new TextDecoder();
16
16
  let buffer = "";
@@ -49,28 +49,54 @@ async function* streamSSE(stream, signal) {
49
49
  } catch {
50
50
  }
51
51
  }
52
- }
53
-
54
- // src/constants.ts
55
- var DEFAULT_PORT = 8567;
56
- var COMPLETED_STATUS = "Completed successfully";
57
-
58
- // src/client.ts
59
- var DEFAULT_SERVER_URL = `http://localhost:${DEFAULT_PORT}`;
60
- async function* streamFromServer(serverUrl, context, signal) {
52
+ };
53
+ var isRecord = (value) => typeof value === "object" && value !== null;
54
+ var getStoredAgentContext = (storage, sessionId, storageKey = STORAGE_KEY) => {
55
+ const rawSessions = storage.getItem(storageKey);
56
+ if (!rawSessions) throw new Error("No sessions to resume");
57
+ let parsed;
58
+ try {
59
+ parsed = JSON.parse(rawSessions);
60
+ } catch {
61
+ throw new Error("Failed to parse stored sessions");
62
+ }
63
+ if (!isRecord(parsed)) throw new Error("Invalid stored sessions");
64
+ const storedSession = parsed[sessionId];
65
+ if (!isRecord(storedSession)) {
66
+ throw new Error(`Session ${sessionId} not found`);
67
+ }
68
+ const context = storedSession.context;
69
+ if (!isRecord(context)) throw new Error(`Session ${sessionId} is invalid`);
70
+ const content = context.content;
71
+ const prompt = context.prompt;
72
+ if (typeof content !== "string" || typeof prompt !== "string") {
73
+ throw new Error(`Session ${sessionId} is invalid`);
74
+ }
75
+ const options = context.options;
76
+ const storedSessionId = context.sessionId;
77
+ return {
78
+ content,
79
+ prompt,
80
+ options,
81
+ sessionId: typeof storedSessionId === "string" ? storedSessionId : void 0
82
+ };
83
+ };
84
+ var streamAgentStatusFromServer = async function* (options, context, signal) {
61
85
  const startTime = Date.now();
62
86
  const sessionId = context.sessionId;
87
+ const pollIntervalMs = options.pollIntervalMs ?? 100;
88
+ const agentUrl = `${options.serverUrl}${options.agentPath ?? "/agent"}`;
63
89
  const handleAbort = () => {
64
- if (sessionId) {
65
- fetch(`${serverUrl}/abort/${sessionId}`, { method: "POST" }).catch(
66
- () => {
67
- }
68
- );
69
- }
90
+ if (!sessionId) return;
91
+ const abortPath = options.abortPath?.(sessionId) ?? `/abort/${sessionId}`;
92
+ fetch(`${options.serverUrl}${abortPath}`, { method: "POST" }).catch(
93
+ () => {
94
+ }
95
+ );
70
96
  };
71
97
  signal.addEventListener("abort", handleAbort);
72
98
  try {
73
- const response = await fetch(`${serverUrl}/agent`, {
99
+ const response = await fetch(agentUrl, {
74
100
  method: "POST",
75
101
  headers: { "Content-Type": "application/json" },
76
102
  body: JSON.stringify(context),
@@ -83,29 +109,29 @@ async function* streamFromServer(serverUrl, context, signal) {
83
109
  throw new Error("No response body");
84
110
  }
85
111
  const iterator = streamSSE(response.body, signal)[Symbol.asyncIterator]();
86
- let done = false;
112
+ let isDone = false;
87
113
  let pendingNext = iterator.next();
88
114
  let lastStatus = null;
89
- while (!done) {
115
+ while (!isDone) {
90
116
  const result = await Promise.race([
91
117
  pendingNext.then((iteratorResult) => ({
92
118
  type: "status",
93
119
  iteratorResult
94
120
  })),
95
121
  new Promise(
96
- (resolve) => setTimeout(() => resolve({ type: "timeout" }), 100)
122
+ (resolve) => setTimeout(() => resolve({ type: "timeout" }), pollIntervalMs)
97
123
  )
98
124
  ]);
99
125
  const elapsedSeconds = (Date.now() - startTime) / 1e3;
100
126
  if (result.type === "status") {
101
127
  const iteratorResult = result.iteratorResult;
102
- done = iteratorResult.done ?? false;
103
- if (!done && iteratorResult.value) {
128
+ isDone = iteratorResult.done ?? false;
129
+ if (!isDone && iteratorResult.value) {
104
130
  lastStatus = iteratorResult.value;
105
131
  pendingNext = iterator.next();
106
132
  }
107
133
  }
108
- if (lastStatus === COMPLETED_STATUS) {
134
+ if (lastStatus === options.completedStatus) {
109
135
  yield `Completed in ${elapsedSeconds.toFixed(1)}s`;
110
136
  } else if (lastStatus) {
111
137
  yield `${lastStatus} ${elapsedSeconds.toFixed(1)}s`;
@@ -116,57 +142,74 @@ async function* streamFromServer(serverUrl, context, signal) {
116
142
  } finally {
117
143
  signal.removeEventListener("abort", handleAbort);
118
144
  }
119
- }
145
+ };
146
+ var createCachedConnectionChecker = (checkConnection, ttlMs = CONNECTION_CHECK_TTL_MS) => {
147
+ let cache = null;
148
+ return async () => {
149
+ const now = Date.now();
150
+ if (cache && now - cache.timestamp < ttlMs) return cache.result;
151
+ try {
152
+ const result = await checkConnection();
153
+ cache = { result, timestamp: now };
154
+ return result;
155
+ } catch {
156
+ cache = { result: false, timestamp: now };
157
+ return false;
158
+ }
159
+ };
160
+ };
161
+
162
+ // src/constants.ts
163
+ var DEFAULT_PORT = 8567;
164
+ var COMPLETED_STATUS = "Completed successfully";
165
+
166
+ // src/client.ts
167
+ var DEFAULT_SERVER_URL = `http://localhost:${DEFAULT_PORT}`;
168
+ var isReactGrabApi = (value) => typeof value === "object" && value !== null && "setAgent" in value;
120
169
  var createGeminiAgentProvider = (providerOptions = {}) => {
121
170
  const { serverUrl = DEFAULT_SERVER_URL, getOptions } = providerOptions;
122
- let connectionCache = null;
123
171
  const mergeOptions = (contextOptions) => ({
124
172
  ...getOptions?.() ?? {},
125
173
  ...contextOptions ?? {}
126
174
  });
175
+ const checkConnection = createCachedConnectionChecker(async () => {
176
+ const response = await fetch(`${serverUrl}/health`, { method: "GET" });
177
+ return response.ok;
178
+ }, CONNECTION_CHECK_TTL_MS);
127
179
  return {
128
180
  send: async function* (context, signal) {
129
181
  const mergedContext = {
130
182
  ...context,
131
183
  options: mergeOptions(context.options)
132
184
  };
133
- yield* streamFromServer(serverUrl, mergedContext, signal);
185
+ yield* streamAgentStatusFromServer(
186
+ { serverUrl, completedStatus: COMPLETED_STATUS },
187
+ mergedContext,
188
+ signal
189
+ );
134
190
  },
135
191
  resume: async function* (sessionId, signal, storage) {
136
- const savedSessions = storage.getItem(STORAGE_KEY);
137
- if (!savedSessions) {
138
- throw new Error("No sessions to resume");
139
- }
140
- const sessionsObject = JSON.parse(savedSessions);
141
- const session = sessionsObject[sessionId];
142
- if (!session) {
143
- throw new Error(`Session ${sessionId} not found`);
144
- }
145
- const context = session.context;
192
+ const storedContext = getStoredAgentContext(storage, sessionId);
193
+ const context = {
194
+ content: storedContext.content,
195
+ prompt: storedContext.prompt,
196
+ options: storedContext.options,
197
+ sessionId: storedContext.sessionId ?? sessionId
198
+ };
146
199
  const mergedContext = {
147
200
  ...context,
148
201
  options: mergeOptions(context.options)
149
202
  };
150
203
  yield "Resuming...";
151
- yield* streamFromServer(serverUrl, mergedContext, signal);
204
+ yield* streamAgentStatusFromServer(
205
+ { serverUrl, completedStatus: COMPLETED_STATUS },
206
+ mergedContext,
207
+ signal
208
+ );
152
209
  },
153
210
  supportsResume: true,
154
211
  supportsFollowUp: true,
155
- checkConnection: async () => {
156
- const now = Date.now();
157
- if (connectionCache && now - connectionCache.timestamp < CONNECTION_CHECK_TTL_MS) {
158
- return connectionCache.result;
159
- }
160
- try {
161
- const response = await fetch(`${serverUrl}/health`, { method: "GET" });
162
- const result = response.ok;
163
- connectionCache = { result, timestamp: now };
164
- return result;
165
- } catch {
166
- connectionCache = { result: false, timestamp: now };
167
- return false;
168
- }
169
- },
212
+ checkConnection,
170
213
  abort: async (sessionId) => {
171
214
  try {
172
215
  await fetch(`${serverUrl}/abort/${sessionId}`, { method: "POST" });
@@ -184,24 +227,25 @@ var createGeminiAgentProvider = (providerOptions = {}) => {
184
227
  var attachAgent = async () => {
185
228
  if (typeof window === "undefined") return;
186
229
  const provider = createGeminiAgentProvider();
187
- const attach = (api2) => {
188
- api2.setAgent({ provider, storage: sessionStorage });
230
+ const attach = (api) => {
231
+ api.setAgent({ provider, storage: sessionStorage });
189
232
  };
190
- const api = window.__REACT_GRAB__;
191
- if (api) {
192
- attach(api);
233
+ const existingApi = window.__REACT_GRAB__;
234
+ if (isReactGrabApi(existingApi)) {
235
+ attach(existingApi);
193
236
  return;
194
237
  }
195
238
  window.addEventListener(
196
239
  "react-grab:init",
197
240
  (event) => {
198
- const customEvent = event;
199
- attach(customEvent.detail);
241
+ if (!(event instanceof CustomEvent)) return;
242
+ if (!isReactGrabApi(event.detail)) return;
243
+ attach(event.detail);
200
244
  },
201
245
  { once: true }
202
246
  );
203
247
  const apiAfterListener = window.__REACT_GRAB__;
204
- if (apiAfterListener) {
248
+ if (isReactGrabApi(apiAfterListener)) {
205
249
  attach(apiAfterListener);
206
250
  }
207
251
  };
package/dist/server.cjs CHANGED
@@ -12129,9 +12129,11 @@ Check that the command is executable: chmod +x $(which ${commandName})`;
12129
12129
  };
12130
12130
 
12131
12131
  // src/server.ts
12132
- var VERSION = "0.0.87";
12132
+ var VERSION = "0.0.89";
12133
12133
  try {
12134
- fetch(`https://www.react-grab.com/api/version?source=gemini&t=${Date.now()}`).catch(() => {
12134
+ fetch(
12135
+ `https://www.react-grab.com/api/version?source=gemini&t=${Date.now()}`
12136
+ ).catch(() => {
12135
12137
  });
12136
12138
  } catch {
12137
12139
  }
@@ -12147,6 +12149,180 @@ var parseStreamLine = (line) => {
12147
12149
  return null;
12148
12150
  }
12149
12151
  };
12152
+ var runAgent = async function* (prompt, options) {
12153
+ const geminiArgs = ["--output-format", "stream-json", "--yolo"];
12154
+ if (options?.model) {
12155
+ geminiArgs.push("--model", options.model);
12156
+ }
12157
+ if (options?.includeDirectories) {
12158
+ geminiArgs.push("--include-directories", options.includeDirectories);
12159
+ }
12160
+ geminiArgs.push(prompt);
12161
+ let geminiProcess;
12162
+ let stderrBuffer = "";
12163
+ try {
12164
+ yield { type: "status", content: "Thinking\u2026" };
12165
+ geminiProcess = execa("gemini", geminiArgs, {
12166
+ stdin: "pipe",
12167
+ stdout: "pipe",
12168
+ stderr: "pipe",
12169
+ env: { ...process.env },
12170
+ cwd: options?.cwd ?? process.env.REACT_GRAB_CWD ?? process.cwd()
12171
+ });
12172
+ if (options?.sessionId) {
12173
+ activeProcesses.set(options.sessionId, geminiProcess);
12174
+ }
12175
+ if (geminiProcess.stderr) {
12176
+ geminiProcess.stderr.on("data", (chunk) => {
12177
+ stderrBuffer += chunk.toString();
12178
+ });
12179
+ }
12180
+ const messageQueue = [];
12181
+ let resolveWait = null;
12182
+ let processEnded = false;
12183
+ let capturedSessionId;
12184
+ const enqueueMessage = (message) => {
12185
+ messageQueue.push(message);
12186
+ if (resolveWait) {
12187
+ resolveWait();
12188
+ resolveWait = null;
12189
+ }
12190
+ };
12191
+ const processLine = (line) => {
12192
+ const event = parseStreamLine(line);
12193
+ if (!event) return;
12194
+ if (!capturedSessionId && event.session_id) {
12195
+ capturedSessionId = event.session_id;
12196
+ }
12197
+ switch (event.type) {
12198
+ case "init":
12199
+ enqueueMessage({ type: "status", content: "Session started..." });
12200
+ break;
12201
+ case "message":
12202
+ if (event.role === "assistant" && event.content) {
12203
+ enqueueMessage({ type: "status", content: event.content });
12204
+ }
12205
+ break;
12206
+ case "tool_use":
12207
+ if (event.tool_name) {
12208
+ enqueueMessage({
12209
+ type: "status",
12210
+ content: `Using ${event.tool_name}...`
12211
+ });
12212
+ }
12213
+ break;
12214
+ case "tool_result":
12215
+ if (event.status === "error" && event.output) {
12216
+ enqueueMessage({
12217
+ type: "status",
12218
+ content: `Tool error: ${event.output}`
12219
+ });
12220
+ }
12221
+ break;
12222
+ case "error":
12223
+ if (event.content) {
12224
+ enqueueMessage({ type: "error", content: event.content });
12225
+ }
12226
+ break;
12227
+ case "result":
12228
+ if (event.status === "success") {
12229
+ enqueueMessage({ type: "status", content: COMPLETED_STATUS });
12230
+ } else if (event.status === "error") {
12231
+ enqueueMessage({ type: "error", content: "Task failed" });
12232
+ }
12233
+ break;
12234
+ }
12235
+ };
12236
+ let buffer = "";
12237
+ if (geminiProcess.stdout) {
12238
+ geminiProcess.stdout.on("data", (chunk) => {
12239
+ buffer += chunk.toString();
12240
+ let newlineIndex;
12241
+ while ((newlineIndex = buffer.indexOf("\n")) !== -1) {
12242
+ const line = buffer.slice(0, newlineIndex);
12243
+ buffer = buffer.slice(newlineIndex + 1);
12244
+ processLine(line);
12245
+ }
12246
+ });
12247
+ }
12248
+ const childProcess4 = geminiProcess;
12249
+ childProcess4.on("close", (code) => {
12250
+ if (options?.sessionId) {
12251
+ activeProcesses.delete(options.sessionId);
12252
+ }
12253
+ if (buffer.trim()) {
12254
+ processLine(buffer);
12255
+ }
12256
+ if (options?.sessionId && capturedSessionId) {
12257
+ geminiSessionMap.set(options.sessionId, capturedSessionId);
12258
+ }
12259
+ if (capturedSessionId) {
12260
+ lastGeminiSessionId = capturedSessionId;
12261
+ }
12262
+ processEnded = true;
12263
+ if (code !== 0 && !childProcess4.killed) {
12264
+ enqueueMessage({
12265
+ type: "error",
12266
+ content: `gemini exited with code ${code}`
12267
+ });
12268
+ }
12269
+ enqueueMessage({ type: "done", content: "" });
12270
+ if (resolveWait) {
12271
+ resolveWait();
12272
+ resolveWait = null;
12273
+ }
12274
+ });
12275
+ childProcess4.on("error", (error) => {
12276
+ if (options?.sessionId) {
12277
+ activeProcesses.delete(options.sessionId);
12278
+ }
12279
+ processEnded = true;
12280
+ const errorMessage = formatSpawnError(error, "gemini");
12281
+ const stderrContent = stderrBuffer.trim();
12282
+ const fullError = stderrContent ? `${errorMessage}
12283
+
12284
+ stderr:
12285
+ ${stderrContent}` : errorMessage;
12286
+ enqueueMessage({ type: "error", content: fullError });
12287
+ enqueueMessage({ type: "done", content: "" });
12288
+ if (resolveWait) {
12289
+ resolveWait();
12290
+ resolveWait = null;
12291
+ }
12292
+ });
12293
+ while (true) {
12294
+ if (options?.signal?.aborted) {
12295
+ if (geminiProcess && !geminiProcess.killed) {
12296
+ geminiProcess.kill("SIGTERM");
12297
+ }
12298
+ return;
12299
+ }
12300
+ if (messageQueue.length > 0) {
12301
+ const message = messageQueue.shift();
12302
+ if (message.type === "done") {
12303
+ yield message;
12304
+ return;
12305
+ }
12306
+ yield message;
12307
+ } else if (processEnded) {
12308
+ return;
12309
+ } else {
12310
+ await new Promise((resolve) => {
12311
+ resolveWait = resolve;
12312
+ });
12313
+ }
12314
+ }
12315
+ } catch (error) {
12316
+ const errorMessage = error instanceof Error ? formatSpawnError(error, "gemini") : "Unknown error";
12317
+ const stderrContent = stderrBuffer.trim();
12318
+ const fullError = stderrContent ? `${errorMessage}
12319
+
12320
+ stderr:
12321
+ ${stderrContent}` : errorMessage;
12322
+ yield { type: "error", content: fullError };
12323
+ yield { type: "done", content: "" };
12324
+ }
12325
+ };
12150
12326
  var createServer = () => {
12151
12327
  const app = new Hono2();
12152
12328
  app.use("*", cors());
@@ -12159,146 +12335,18 @@ var createServer = () => {
12159
12335
 
12160
12336
  ${content}`;
12161
12337
  return streamSSE(context, async (stream2) => {
12162
- const geminiArgs = ["--output-format", "stream-json", "--yolo"];
12163
- if (options?.model) {
12164
- geminiArgs.push("--model", options.model);
12165
- }
12166
- if (options?.includeDirectories) {
12167
- geminiArgs.push("--include-directories", options.includeDirectories);
12168
- }
12169
- geminiArgs.push(userPrompt);
12170
- let geminiProcess;
12171
- let stderrBuffer = "";
12172
- try {
12173
- await stream2.writeSSE({ data: "Thinking\u2026", event: "status" });
12174
- geminiProcess = execa("gemini", geminiArgs, {
12175
- stdin: "pipe",
12176
- stdout: "pipe",
12177
- stderr: "pipe",
12178
- env: { ...process.env },
12179
- cwd: process.env.REACT_GRAB_CWD ?? process.cwd()
12180
- });
12181
- if (sessionId) {
12182
- activeProcesses.set(sessionId, geminiProcess);
12183
- }
12184
- if (geminiProcess.stderr) {
12185
- geminiProcess.stderr.on("data", (chunk) => {
12186
- stderrBuffer += chunk.toString();
12338
+ for await (const message of runAgent(userPrompt, {
12339
+ ...options,
12340
+ sessionId
12341
+ })) {
12342
+ if (message.type === "error") {
12343
+ await stream2.writeSSE({
12344
+ data: `Error: ${message.content}`,
12345
+ event: "error"
12187
12346
  });
12347
+ } else {
12348
+ await stream2.writeSSE({ data: message.content, event: message.type });
12188
12349
  }
12189
- let buffer = "";
12190
- let capturedSessionId;
12191
- const processLine = async (line) => {
12192
- const event = parseStreamLine(line);
12193
- if (!event) return;
12194
- if (!capturedSessionId && event.session_id) {
12195
- capturedSessionId = event.session_id;
12196
- }
12197
- switch (event.type) {
12198
- case "init":
12199
- await stream2.writeSSE({
12200
- data: "Session started...",
12201
- event: "status"
12202
- });
12203
- break;
12204
- case "message":
12205
- if (event.role === "assistant" && event.content) {
12206
- await stream2.writeSSE({ data: event.content, event: "status" });
12207
- }
12208
- break;
12209
- case "tool_use":
12210
- if (event.tool_name) {
12211
- await stream2.writeSSE({
12212
- data: `Using ${event.tool_name}...`,
12213
- event: "status"
12214
- });
12215
- }
12216
- break;
12217
- case "tool_result":
12218
- if (event.status === "error" && event.output) {
12219
- await stream2.writeSSE({
12220
- data: `Tool error: ${event.output}`,
12221
- event: "status"
12222
- });
12223
- }
12224
- break;
12225
- case "error":
12226
- if (event.content) {
12227
- await stream2.writeSSE({
12228
- data: `Error: ${event.content}`,
12229
- event: "error"
12230
- });
12231
- }
12232
- break;
12233
- case "result":
12234
- if (event.status === "success") {
12235
- await stream2.writeSSE({
12236
- data: COMPLETED_STATUS,
12237
- event: "status"
12238
- });
12239
- } else if (event.status === "error") {
12240
- await stream2.writeSSE({
12241
- data: "Task failed",
12242
- event: "error"
12243
- });
12244
- }
12245
- break;
12246
- }
12247
- };
12248
- if (geminiProcess.stdout) {
12249
- geminiProcess.stdout.on("data", async (chunk) => {
12250
- buffer += chunk.toString();
12251
- let newlineIndex;
12252
- while ((newlineIndex = buffer.indexOf("\n")) !== -1) {
12253
- const line = buffer.slice(0, newlineIndex);
12254
- buffer = buffer.slice(newlineIndex + 1);
12255
- await processLine(line);
12256
- }
12257
- });
12258
- }
12259
- if (geminiProcess) {
12260
- const childProcess4 = geminiProcess;
12261
- await new Promise((resolve, reject) => {
12262
- childProcess4.on("close", (code) => {
12263
- if (sessionId) {
12264
- activeProcesses.delete(sessionId);
12265
- }
12266
- if (code === 0 || childProcess4.killed) {
12267
- resolve();
12268
- } else {
12269
- reject(new Error(`gemini exited with code ${code}`));
12270
- }
12271
- });
12272
- childProcess4.on("error", (error) => {
12273
- if (sessionId) {
12274
- activeProcesses.delete(sessionId);
12275
- }
12276
- reject(error);
12277
- });
12278
- });
12279
- }
12280
- if (buffer.trim()) {
12281
- await processLine(buffer);
12282
- }
12283
- if (sessionId && capturedSessionId) {
12284
- geminiSessionMap.set(sessionId, capturedSessionId);
12285
- }
12286
- if (capturedSessionId) {
12287
- lastGeminiSessionId = capturedSessionId;
12288
- }
12289
- await stream2.writeSSE({ data: "", event: "done" });
12290
- } catch (error) {
12291
- const errorMessage = error instanceof Error ? formatSpawnError(error, "gemini") : "Unknown error";
12292
- const stderrContent = stderrBuffer.trim();
12293
- const fullError = stderrContent ? `${errorMessage}
12294
-
12295
- stderr:
12296
- ${stderrContent}` : errorMessage;
12297
- await stream2.writeSSE({
12298
- data: `Error: ${fullError}`,
12299
- event: "error"
12300
- });
12301
- await stream2.writeSSE({ data: "", event: "done" });
12302
12350
  }
12303
12351
  });
12304
12352
  });
@@ -12357,4 +12405,5 @@ if ((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filen
12357
12405
  }
12358
12406
 
12359
12407
  exports.createServer = createServer;
12408
+ exports.runAgent = runAgent;
12360
12409
  exports.startServer = startServer;
package/dist/server.d.cts CHANGED
@@ -1,7 +1,13 @@
1
1
  import * as hono_types from 'hono/types';
2
2
  import { Hono } from 'hono';
3
+ import { AgentCoreOptions, AgentMessage } from '@react-grab/utils/server';
3
4
 
5
+ interface GeminiAgentOptions extends AgentCoreOptions {
6
+ model?: string;
7
+ includeDirectories?: string;
8
+ }
9
+ declare const runAgent: (prompt: string, options?: GeminiAgentOptions) => AsyncGenerator<AgentMessage>;
4
10
  declare const createServer: () => Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
5
11
  declare const startServer: (port?: number) => Promise<void>;
6
12
 
7
- export { createServer, startServer };
13
+ export { type GeminiAgentOptions, createServer, runAgent, startServer };
package/dist/server.d.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  import * as hono_types from 'hono/types';
2
2
  import { Hono } from 'hono';
3
+ import { AgentCoreOptions, AgentMessage } from '@react-grab/utils/server';
3
4
 
5
+ interface GeminiAgentOptions extends AgentCoreOptions {
6
+ model?: string;
7
+ includeDirectories?: string;
8
+ }
9
+ declare const runAgent: (prompt: string, options?: GeminiAgentOptions) => AsyncGenerator<AgentMessage>;
4
10
  declare const createServer: () => Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
5
11
  declare const startServer: (port?: number) => Promise<void>;
6
12
 
7
- export { createServer, startServer };
13
+ export { type GeminiAgentOptions, createServer, runAgent, startServer };
package/dist/server.js CHANGED
@@ -12117,9 +12117,11 @@ Check that the command is executable: chmod +x $(which ${commandName})`;
12117
12117
  };
12118
12118
 
12119
12119
  // src/server.ts
12120
- var VERSION = "0.0.87";
12120
+ var VERSION = "0.0.89";
12121
12121
  try {
12122
- fetch(`https://www.react-grab.com/api/version?source=gemini&t=${Date.now()}`).catch(() => {
12122
+ fetch(
12123
+ `https://www.react-grab.com/api/version?source=gemini&t=${Date.now()}`
12124
+ ).catch(() => {
12123
12125
  });
12124
12126
  } catch {
12125
12127
  }
@@ -12135,6 +12137,180 @@ var parseStreamLine = (line) => {
12135
12137
  return null;
12136
12138
  }
12137
12139
  };
12140
+ var runAgent = async function* (prompt, options) {
12141
+ const geminiArgs = ["--output-format", "stream-json", "--yolo"];
12142
+ if (options?.model) {
12143
+ geminiArgs.push("--model", options.model);
12144
+ }
12145
+ if (options?.includeDirectories) {
12146
+ geminiArgs.push("--include-directories", options.includeDirectories);
12147
+ }
12148
+ geminiArgs.push(prompt);
12149
+ let geminiProcess;
12150
+ let stderrBuffer = "";
12151
+ try {
12152
+ yield { type: "status", content: "Thinking\u2026" };
12153
+ geminiProcess = execa("gemini", geminiArgs, {
12154
+ stdin: "pipe",
12155
+ stdout: "pipe",
12156
+ stderr: "pipe",
12157
+ env: { ...process.env },
12158
+ cwd: options?.cwd ?? process.env.REACT_GRAB_CWD ?? process.cwd()
12159
+ });
12160
+ if (options?.sessionId) {
12161
+ activeProcesses.set(options.sessionId, geminiProcess);
12162
+ }
12163
+ if (geminiProcess.stderr) {
12164
+ geminiProcess.stderr.on("data", (chunk) => {
12165
+ stderrBuffer += chunk.toString();
12166
+ });
12167
+ }
12168
+ const messageQueue = [];
12169
+ let resolveWait = null;
12170
+ let processEnded = false;
12171
+ let capturedSessionId;
12172
+ const enqueueMessage = (message) => {
12173
+ messageQueue.push(message);
12174
+ if (resolveWait) {
12175
+ resolveWait();
12176
+ resolveWait = null;
12177
+ }
12178
+ };
12179
+ const processLine = (line) => {
12180
+ const event = parseStreamLine(line);
12181
+ if (!event) return;
12182
+ if (!capturedSessionId && event.session_id) {
12183
+ capturedSessionId = event.session_id;
12184
+ }
12185
+ switch (event.type) {
12186
+ case "init":
12187
+ enqueueMessage({ type: "status", content: "Session started..." });
12188
+ break;
12189
+ case "message":
12190
+ if (event.role === "assistant" && event.content) {
12191
+ enqueueMessage({ type: "status", content: event.content });
12192
+ }
12193
+ break;
12194
+ case "tool_use":
12195
+ if (event.tool_name) {
12196
+ enqueueMessage({
12197
+ type: "status",
12198
+ content: `Using ${event.tool_name}...`
12199
+ });
12200
+ }
12201
+ break;
12202
+ case "tool_result":
12203
+ if (event.status === "error" && event.output) {
12204
+ enqueueMessage({
12205
+ type: "status",
12206
+ content: `Tool error: ${event.output}`
12207
+ });
12208
+ }
12209
+ break;
12210
+ case "error":
12211
+ if (event.content) {
12212
+ enqueueMessage({ type: "error", content: event.content });
12213
+ }
12214
+ break;
12215
+ case "result":
12216
+ if (event.status === "success") {
12217
+ enqueueMessage({ type: "status", content: COMPLETED_STATUS });
12218
+ } else if (event.status === "error") {
12219
+ enqueueMessage({ type: "error", content: "Task failed" });
12220
+ }
12221
+ break;
12222
+ }
12223
+ };
12224
+ let buffer = "";
12225
+ if (geminiProcess.stdout) {
12226
+ geminiProcess.stdout.on("data", (chunk) => {
12227
+ buffer += chunk.toString();
12228
+ let newlineIndex;
12229
+ while ((newlineIndex = buffer.indexOf("\n")) !== -1) {
12230
+ const line = buffer.slice(0, newlineIndex);
12231
+ buffer = buffer.slice(newlineIndex + 1);
12232
+ processLine(line);
12233
+ }
12234
+ });
12235
+ }
12236
+ const childProcess4 = geminiProcess;
12237
+ childProcess4.on("close", (code) => {
12238
+ if (options?.sessionId) {
12239
+ activeProcesses.delete(options.sessionId);
12240
+ }
12241
+ if (buffer.trim()) {
12242
+ processLine(buffer);
12243
+ }
12244
+ if (options?.sessionId && capturedSessionId) {
12245
+ geminiSessionMap.set(options.sessionId, capturedSessionId);
12246
+ }
12247
+ if (capturedSessionId) {
12248
+ lastGeminiSessionId = capturedSessionId;
12249
+ }
12250
+ processEnded = true;
12251
+ if (code !== 0 && !childProcess4.killed) {
12252
+ enqueueMessage({
12253
+ type: "error",
12254
+ content: `gemini exited with code ${code}`
12255
+ });
12256
+ }
12257
+ enqueueMessage({ type: "done", content: "" });
12258
+ if (resolveWait) {
12259
+ resolveWait();
12260
+ resolveWait = null;
12261
+ }
12262
+ });
12263
+ childProcess4.on("error", (error) => {
12264
+ if (options?.sessionId) {
12265
+ activeProcesses.delete(options.sessionId);
12266
+ }
12267
+ processEnded = true;
12268
+ const errorMessage = formatSpawnError(error, "gemini");
12269
+ const stderrContent = stderrBuffer.trim();
12270
+ const fullError = stderrContent ? `${errorMessage}
12271
+
12272
+ stderr:
12273
+ ${stderrContent}` : errorMessage;
12274
+ enqueueMessage({ type: "error", content: fullError });
12275
+ enqueueMessage({ type: "done", content: "" });
12276
+ if (resolveWait) {
12277
+ resolveWait();
12278
+ resolveWait = null;
12279
+ }
12280
+ });
12281
+ while (true) {
12282
+ if (options?.signal?.aborted) {
12283
+ if (geminiProcess && !geminiProcess.killed) {
12284
+ geminiProcess.kill("SIGTERM");
12285
+ }
12286
+ return;
12287
+ }
12288
+ if (messageQueue.length > 0) {
12289
+ const message = messageQueue.shift();
12290
+ if (message.type === "done") {
12291
+ yield message;
12292
+ return;
12293
+ }
12294
+ yield message;
12295
+ } else if (processEnded) {
12296
+ return;
12297
+ } else {
12298
+ await new Promise((resolve) => {
12299
+ resolveWait = resolve;
12300
+ });
12301
+ }
12302
+ }
12303
+ } catch (error) {
12304
+ const errorMessage = error instanceof Error ? formatSpawnError(error, "gemini") : "Unknown error";
12305
+ const stderrContent = stderrBuffer.trim();
12306
+ const fullError = stderrContent ? `${errorMessage}
12307
+
12308
+ stderr:
12309
+ ${stderrContent}` : errorMessage;
12310
+ yield { type: "error", content: fullError };
12311
+ yield { type: "done", content: "" };
12312
+ }
12313
+ };
12138
12314
  var createServer = () => {
12139
12315
  const app = new Hono2();
12140
12316
  app.use("*", cors());
@@ -12147,146 +12323,18 @@ var createServer = () => {
12147
12323
 
12148
12324
  ${content}`;
12149
12325
  return streamSSE(context, async (stream2) => {
12150
- const geminiArgs = ["--output-format", "stream-json", "--yolo"];
12151
- if (options?.model) {
12152
- geminiArgs.push("--model", options.model);
12153
- }
12154
- if (options?.includeDirectories) {
12155
- geminiArgs.push("--include-directories", options.includeDirectories);
12156
- }
12157
- geminiArgs.push(userPrompt);
12158
- let geminiProcess;
12159
- let stderrBuffer = "";
12160
- try {
12161
- await stream2.writeSSE({ data: "Thinking\u2026", event: "status" });
12162
- geminiProcess = execa("gemini", geminiArgs, {
12163
- stdin: "pipe",
12164
- stdout: "pipe",
12165
- stderr: "pipe",
12166
- env: { ...process.env },
12167
- cwd: process.env.REACT_GRAB_CWD ?? process.cwd()
12168
- });
12169
- if (sessionId) {
12170
- activeProcesses.set(sessionId, geminiProcess);
12171
- }
12172
- if (geminiProcess.stderr) {
12173
- geminiProcess.stderr.on("data", (chunk) => {
12174
- stderrBuffer += chunk.toString();
12326
+ for await (const message of runAgent(userPrompt, {
12327
+ ...options,
12328
+ sessionId
12329
+ })) {
12330
+ if (message.type === "error") {
12331
+ await stream2.writeSSE({
12332
+ data: `Error: ${message.content}`,
12333
+ event: "error"
12175
12334
  });
12335
+ } else {
12336
+ await stream2.writeSSE({ data: message.content, event: message.type });
12176
12337
  }
12177
- let buffer = "";
12178
- let capturedSessionId;
12179
- const processLine = async (line) => {
12180
- const event = parseStreamLine(line);
12181
- if (!event) return;
12182
- if (!capturedSessionId && event.session_id) {
12183
- capturedSessionId = event.session_id;
12184
- }
12185
- switch (event.type) {
12186
- case "init":
12187
- await stream2.writeSSE({
12188
- data: "Session started...",
12189
- event: "status"
12190
- });
12191
- break;
12192
- case "message":
12193
- if (event.role === "assistant" && event.content) {
12194
- await stream2.writeSSE({ data: event.content, event: "status" });
12195
- }
12196
- break;
12197
- case "tool_use":
12198
- if (event.tool_name) {
12199
- await stream2.writeSSE({
12200
- data: `Using ${event.tool_name}...`,
12201
- event: "status"
12202
- });
12203
- }
12204
- break;
12205
- case "tool_result":
12206
- if (event.status === "error" && event.output) {
12207
- await stream2.writeSSE({
12208
- data: `Tool error: ${event.output}`,
12209
- event: "status"
12210
- });
12211
- }
12212
- break;
12213
- case "error":
12214
- if (event.content) {
12215
- await stream2.writeSSE({
12216
- data: `Error: ${event.content}`,
12217
- event: "error"
12218
- });
12219
- }
12220
- break;
12221
- case "result":
12222
- if (event.status === "success") {
12223
- await stream2.writeSSE({
12224
- data: COMPLETED_STATUS,
12225
- event: "status"
12226
- });
12227
- } else if (event.status === "error") {
12228
- await stream2.writeSSE({
12229
- data: "Task failed",
12230
- event: "error"
12231
- });
12232
- }
12233
- break;
12234
- }
12235
- };
12236
- if (geminiProcess.stdout) {
12237
- geminiProcess.stdout.on("data", async (chunk) => {
12238
- buffer += chunk.toString();
12239
- let newlineIndex;
12240
- while ((newlineIndex = buffer.indexOf("\n")) !== -1) {
12241
- const line = buffer.slice(0, newlineIndex);
12242
- buffer = buffer.slice(newlineIndex + 1);
12243
- await processLine(line);
12244
- }
12245
- });
12246
- }
12247
- if (geminiProcess) {
12248
- const childProcess4 = geminiProcess;
12249
- await new Promise((resolve, reject) => {
12250
- childProcess4.on("close", (code) => {
12251
- if (sessionId) {
12252
- activeProcesses.delete(sessionId);
12253
- }
12254
- if (code === 0 || childProcess4.killed) {
12255
- resolve();
12256
- } else {
12257
- reject(new Error(`gemini exited with code ${code}`));
12258
- }
12259
- });
12260
- childProcess4.on("error", (error) => {
12261
- if (sessionId) {
12262
- activeProcesses.delete(sessionId);
12263
- }
12264
- reject(error);
12265
- });
12266
- });
12267
- }
12268
- if (buffer.trim()) {
12269
- await processLine(buffer);
12270
- }
12271
- if (sessionId && capturedSessionId) {
12272
- geminiSessionMap.set(sessionId, capturedSessionId);
12273
- }
12274
- if (capturedSessionId) {
12275
- lastGeminiSessionId = capturedSessionId;
12276
- }
12277
- await stream2.writeSSE({ data: "", event: "done" });
12278
- } catch (error) {
12279
- const errorMessage = error instanceof Error ? formatSpawnError(error, "gemini") : "Unknown error";
12280
- const stderrContent = stderrBuffer.trim();
12281
- const fullError = stderrContent ? `${errorMessage}
12282
-
12283
- stderr:
12284
- ${stderrContent}` : errorMessage;
12285
- await stream2.writeSSE({
12286
- data: `Error: ${fullError}`,
12287
- event: "error"
12288
- });
12289
- await stream2.writeSSE({ data: "", event: "done" });
12290
12338
  }
12291
12339
  });
12292
12340
  });
@@ -12344,4 +12392,4 @@ if (import.meta.url === pathToFileURL(process.argv[1]).href) {
12344
12392
  startServer(DEFAULT_PORT).catch(console.error);
12345
12393
  }
12346
12394
 
12347
- export { createServer, startServer };
12395
+ export { createServer, runAgent, startServer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-grab/gemini",
3
- "version": "0.0.87",
3
+ "version": "0.0.89",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "react-grab-gemini": "./dist/cli.cjs"
@@ -26,7 +26,7 @@
26
26
  "devDependencies": {
27
27
  "@types/node": "^22.10.7",
28
28
  "tsup": "^8.4.0",
29
- "@react-grab/utils": "0.0.87"
29
+ "@react-grab/utils": "0.0.89"
30
30
  },
31
31
  "dependencies": {
32
32
  "@hono/node-server": "^1.19.6",
@@ -34,7 +34,7 @@
34
34
  "fkill": "^9.0.0",
35
35
  "hono": "^4.0.0",
36
36
  "picocolors": "^1.1.1",
37
- "react-grab": "0.0.87"
37
+ "react-grab": "0.0.89"
38
38
  },
39
39
  "scripts": {
40
40
  "dev": "tsup --watch",