orquesta-cli 0.2.39 โ 0.2.41
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/agents/planner/index.d.ts +1 -0
- package/dist/agents/planner/index.js +57 -0
- package/dist/core/slash-command-handler.js +36 -0
- package/dist/orquesta/remote-phone.d.ts +52 -0
- package/dist/orquesta/remote-phone.js +214 -0
- package/dist/tools/llm/simple/remote-phone-tool.d.ts +4 -0
- package/dist/tools/llm/simple/remote-phone-tool.js +55 -0
- package/dist/tools/registry.js +2 -0
- package/dist/ui/components/PlanExecuteApp.js +49 -0
- package/dist/ui/hooks/slashCommandProcessor.js +5 -0
- package/package.json +1 -1
|
@@ -14,5 +14,6 @@ export declare class PlanningLLM {
|
|
|
14
14
|
generateTODOList(userRequest: string, contextMessages?: Message[]): Promise<PlanningResult>;
|
|
15
15
|
generateTODOListWithDocsDecision(userRequest: string, contextMessages?: Message[]): Promise<PlanningWithDocsResult>;
|
|
16
16
|
}
|
|
17
|
+
export declare function extractLeakedToolCallName(text: string): string | null;
|
|
17
18
|
export default PlanningLLM;
|
|
18
19
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -195,6 +195,14 @@ Choose one of your 3 tools now.`,
|
|
|
195
195
|
lastError = new Error('Planning LLM returned empty response.');
|
|
196
196
|
continue;
|
|
197
197
|
}
|
|
198
|
+
const leakedTool = extractLeakedToolCallName(responseText);
|
|
199
|
+
if (leakedTool) {
|
|
200
|
+
logger.warn('respond_to_user response is a leaked tool-call JSON', { leakedTool });
|
|
201
|
+
lastError = new Error(`You attempted to call '${leakedTool}', which is an Execution LLM tool. ` +
|
|
202
|
+
`As the PLANNING LLM you must use create_todos for action requests ` +
|
|
203
|
+
`(the Execution LLM will run '${leakedTool}' for you) or respond_to_user with plain prose.`);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
198
206
|
return {
|
|
199
207
|
todos: [],
|
|
200
208
|
complexity: 'simple',
|
|
@@ -254,5 +262,54 @@ Choose one of your 3 tools now.`,
|
|
|
254
262
|
};
|
|
255
263
|
}
|
|
256
264
|
}
|
|
265
|
+
const PLANNING_TOOL_NAMES = new Set(['ask_to_user', 'create_todos', 'respond_to_user']);
|
|
266
|
+
export function extractLeakedToolCallName(text) {
|
|
267
|
+
for (let start = text.indexOf('{'); start !== -1; start = text.indexOf('{', start + 1)) {
|
|
268
|
+
const end = findBalancedObjectEnd(text, start);
|
|
269
|
+
if (end === -1)
|
|
270
|
+
continue;
|
|
271
|
+
let parsed;
|
|
272
|
+
try {
|
|
273
|
+
parsed = JSON.parse(text.slice(start, end + 1));
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (parsed && typeof parsed === 'object') {
|
|
279
|
+
const tool = parsed.tool;
|
|
280
|
+
if (typeof tool === 'string' && tool && !PLANNING_TOOL_NAMES.has(tool))
|
|
281
|
+
return tool;
|
|
282
|
+
start = end;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
function findBalancedObjectEnd(s, start) {
|
|
288
|
+
let depth = 0;
|
|
289
|
+
let inString = false;
|
|
290
|
+
let escaped = false;
|
|
291
|
+
for (let i = start; i < s.length; i++) {
|
|
292
|
+
const c = s[i];
|
|
293
|
+
if (inString) {
|
|
294
|
+
if (escaped)
|
|
295
|
+
escaped = false;
|
|
296
|
+
else if (c === '\\')
|
|
297
|
+
escaped = true;
|
|
298
|
+
else if (c === '"')
|
|
299
|
+
inString = false;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (c === '"')
|
|
303
|
+
inString = true;
|
|
304
|
+
else if (c === '{')
|
|
305
|
+
depth++;
|
|
306
|
+
else if (c === '}') {
|
|
307
|
+
depth--;
|
|
308
|
+
if (depth === 0)
|
|
309
|
+
return i;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return -1;
|
|
313
|
+
}
|
|
257
314
|
export default PlanningLLM;
|
|
258
315
|
//# sourceMappingURL=index.js.map
|
|
@@ -8,6 +8,7 @@ import { createRequire } from 'module';
|
|
|
8
8
|
import { configManager } from './config/config-manager.js';
|
|
9
9
|
import { getForcedTier, setForcedTier, resetBatutaSession } from './routing-state.js';
|
|
10
10
|
import { auditLog } from '../orchestration/audit-log.js';
|
|
11
|
+
import { remotePhone } from '../orquesta/remote-phone.js';
|
|
11
12
|
export async function executeSlashCommand(command, context) {
|
|
12
13
|
const trimmedCommand = command.trim();
|
|
13
14
|
logger.enter('executeSlashCommand', { command: trimmedCommand });
|
|
@@ -591,6 +592,40 @@ ${executorLines}
|
|
|
591
592
|
}
|
|
592
593
|
return reply('Usage: /hook status | /hook enable | /hook disable\nThe hook mirrors claude AND orquesta sessions in this directory into your project.\n(To move a directory to a different project, run `orquesta hook switch` in a shell.)');
|
|
593
594
|
}
|
|
595
|
+
if (trimmedCommand === '/remote-phone' || trimmedCommand.startsWith('/remote-phone ')) {
|
|
596
|
+
logger.flow('Remote-phone command received');
|
|
597
|
+
const sub = (trimmedCommand.split(/\s+/)[1] || 'on').toLowerCase();
|
|
598
|
+
const reply = (content) => {
|
|
599
|
+
const updatedMessages = [...context.messages, { role: 'assistant', content }];
|
|
600
|
+
context.setMessages(updatedMessages);
|
|
601
|
+
return { handled: true, shouldContinue: false, updatedContext: { messages: updatedMessages } };
|
|
602
|
+
};
|
|
603
|
+
if (sub === 'status') {
|
|
604
|
+
return reply(remotePhone.isActive()
|
|
605
|
+
? '๐ฑ Remote phone: ACTIVE โ listening for your phone messages and replying to the channel.\n Stop it with /remote-phone off.'
|
|
606
|
+
: '๐ฑ Remote phone: not active. Start it with /remote-phone on.');
|
|
607
|
+
}
|
|
608
|
+
if (sub === 'off' || sub === 'stop') {
|
|
609
|
+
if (!remotePhone.isActive())
|
|
610
|
+
return reply('Nothing to do โ no remote phone channel is active.');
|
|
611
|
+
await remotePhone.close();
|
|
612
|
+
return reply('๐ฑ Remote phone channel closed. Your phone can no longer drive this session.');
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
const h = await remotePhone.open();
|
|
616
|
+
return reply([
|
|
617
|
+
'๐ฑ Remote phone channel is LIVE โ drive this session from your phone.',
|
|
618
|
+
'',
|
|
619
|
+
h.operatorHandoff,
|
|
620
|
+
...(h.qrAsciiClean ? ['', h.qrAsciiClean] : []),
|
|
621
|
+
'',
|
|
622
|
+
`Channel: ${h.channelId} ยท PIN: ${h.pin} ยท valid 24h. Stop anytime with /remote-phone off.`,
|
|
623
|
+
].join('\n'));
|
|
624
|
+
}
|
|
625
|
+
catch (e) {
|
|
626
|
+
return reply(`โ Could not open the remote phone channel: ${e.message}`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
594
629
|
if (trimmedCommand === '/help') {
|
|
595
630
|
const helpMessage = `
|
|
596
631
|
Available commands:
|
|
@@ -610,6 +645,7 @@ Available commands:
|
|
|
610
645
|
/logout - Sign out of Orquesta (clears token, keeps local LLM configs)
|
|
611
646
|
/whoami - Show current Orquesta connection
|
|
612
647
|
/hook - Claude Code hook here: /hook status | enable | disable
|
|
648
|
+
/remote-phone - Drive this session from your phone: on | off | status
|
|
613
649
|
/update - Update orquesta-cli to the latest version
|
|
614
650
|
|
|
615
651
|
Keyboard shortcuts:
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export declare const DEFAULT_REMOTE_ORIGIN = "https://rogerthat.chat";
|
|
2
|
+
export interface RemotePhoneMessage {
|
|
3
|
+
id: number;
|
|
4
|
+
from: string;
|
|
5
|
+
text: string;
|
|
6
|
+
at: number;
|
|
7
|
+
}
|
|
8
|
+
export interface RemotePhoneHandoff {
|
|
9
|
+
channelId: string;
|
|
10
|
+
mobileUrl: string;
|
|
11
|
+
mobileUrlProtected: string;
|
|
12
|
+
pin: string;
|
|
13
|
+
operatorHandoff: string;
|
|
14
|
+
qrAsciiClean?: string;
|
|
15
|
+
origin: string;
|
|
16
|
+
}
|
|
17
|
+
type MessageListener = (msg: RemotePhoneMessage) => void;
|
|
18
|
+
type NoticeListener = (text: string) => void;
|
|
19
|
+
declare class RemotePhoneController {
|
|
20
|
+
private origin;
|
|
21
|
+
private channelId;
|
|
22
|
+
private channelToken;
|
|
23
|
+
private callsign;
|
|
24
|
+
private identityKey;
|
|
25
|
+
private ownerPassword;
|
|
26
|
+
private sessionId;
|
|
27
|
+
private receiving;
|
|
28
|
+
private abort;
|
|
29
|
+
private lastSeenId;
|
|
30
|
+
private lastPostedReply;
|
|
31
|
+
private messageListeners;
|
|
32
|
+
private noticeListeners;
|
|
33
|
+
isActive(): boolean;
|
|
34
|
+
onMessage(cb: MessageListener): () => void;
|
|
35
|
+
onNotice(cb: NoticeListener): () => void;
|
|
36
|
+
private notice;
|
|
37
|
+
open(opts?: {
|
|
38
|
+
origin?: string;
|
|
39
|
+
}): Promise<RemotePhoneHandoff>;
|
|
40
|
+
private cachedHandoff;
|
|
41
|
+
private buildHandoff;
|
|
42
|
+
private join;
|
|
43
|
+
private startReceiving;
|
|
44
|
+
private receiveLoop;
|
|
45
|
+
postReply(text: string): Promise<void>;
|
|
46
|
+
sendStatus(text: string): Promise<void>;
|
|
47
|
+
private send;
|
|
48
|
+
close(): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
export declare const remotePhone: RemotePhoneController;
|
|
51
|
+
export {};
|
|
52
|
+
//# sourceMappingURL=remote-phone.d.ts.map
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { logger } from '../utils/logger.js';
|
|
2
|
+
export const DEFAULT_REMOTE_ORIGIN = 'https://rogerthat.chat';
|
|
3
|
+
class RemotePhoneController {
|
|
4
|
+
origin = DEFAULT_REMOTE_ORIGIN;
|
|
5
|
+
channelId = null;
|
|
6
|
+
channelToken = null;
|
|
7
|
+
callsign = null;
|
|
8
|
+
identityKey = null;
|
|
9
|
+
ownerPassword = null;
|
|
10
|
+
sessionId = null;
|
|
11
|
+
receiving = false;
|
|
12
|
+
abort = null;
|
|
13
|
+
lastSeenId = 0;
|
|
14
|
+
lastPostedReply = '';
|
|
15
|
+
messageListeners = new Set();
|
|
16
|
+
noticeListeners = new Set();
|
|
17
|
+
isActive() {
|
|
18
|
+
return this.channelId !== null && this.receiving;
|
|
19
|
+
}
|
|
20
|
+
onMessage(cb) {
|
|
21
|
+
this.messageListeners.add(cb);
|
|
22
|
+
return () => this.messageListeners.delete(cb);
|
|
23
|
+
}
|
|
24
|
+
onNotice(cb) {
|
|
25
|
+
this.noticeListeners.add(cb);
|
|
26
|
+
return () => this.noticeListeners.delete(cb);
|
|
27
|
+
}
|
|
28
|
+
notice(text) {
|
|
29
|
+
for (const cb of this.noticeListeners) {
|
|
30
|
+
try {
|
|
31
|
+
cb(text);
|
|
32
|
+
}
|
|
33
|
+
catch { }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async open(opts) {
|
|
37
|
+
if (this.isActive() && this.channelId) {
|
|
38
|
+
return this.buildHandoff(this.cachedHandoff);
|
|
39
|
+
}
|
|
40
|
+
this.origin = (opts?.origin || DEFAULT_REMOTE_ORIGIN).replace(/\/+$/, '');
|
|
41
|
+
logger.flow('remote-phone: minting channel', { origin: this.origin });
|
|
42
|
+
const res = await fetch(`${this.origin}/api/remote-control`, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
body: '{}',
|
|
46
|
+
});
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
const body = await res.text().catch(() => '');
|
|
49
|
+
throw new Error(`remote-control mint failed: HTTP ${res.status} ${body.slice(0, 200)}`);
|
|
50
|
+
}
|
|
51
|
+
const bundle = (await res.json());
|
|
52
|
+
if (!bundle.channel_id || !bundle.channel_token || !bundle.agent?.identity_key) {
|
|
53
|
+
throw new Error('remote-control response missing channel/agent fields');
|
|
54
|
+
}
|
|
55
|
+
this.channelId = bundle.channel_id;
|
|
56
|
+
this.channelToken = bundle.channel_token;
|
|
57
|
+
this.callsign = bundle.agent.callsign;
|
|
58
|
+
this.identityKey = bundle.agent.identity_key;
|
|
59
|
+
this.ownerPassword = bundle.owner_password;
|
|
60
|
+
this.lastSeenId = 0;
|
|
61
|
+
this.lastPostedReply = '';
|
|
62
|
+
this.cachedHandoff = bundle;
|
|
63
|
+
await this.join();
|
|
64
|
+
this.startReceiving();
|
|
65
|
+
return this.buildHandoff(bundle);
|
|
66
|
+
}
|
|
67
|
+
cachedHandoff = null;
|
|
68
|
+
buildHandoff(b) {
|
|
69
|
+
return {
|
|
70
|
+
channelId: b.channel_id,
|
|
71
|
+
mobileUrl: b.mobile_url,
|
|
72
|
+
mobileUrlProtected: b.mobile_url_protected,
|
|
73
|
+
pin: b.owner_password,
|
|
74
|
+
operatorHandoff: b.operator_handoff,
|
|
75
|
+
qrAsciiClean: b.qr_ascii_clean,
|
|
76
|
+
origin: this.origin,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async join() {
|
|
80
|
+
const res = await fetch(`${this.origin}/api/channels/${this.channelId}/join`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: {
|
|
83
|
+
'Authorization': `Bearer ${this.channelToken}`,
|
|
84
|
+
'Content-Type': 'application/json',
|
|
85
|
+
...(this.sessionId ? { 'X-Session-Id': this.sessionId } : {}),
|
|
86
|
+
},
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
callsign: this.callsign,
|
|
89
|
+
identity_key: this.identityKey,
|
|
90
|
+
owner_password: this.ownerPassword,
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
const body = await res.text().catch(() => '');
|
|
95
|
+
throw new Error(`channel join failed: HTTP ${res.status} ${body.slice(0, 200)}`);
|
|
96
|
+
}
|
|
97
|
+
const data = (await res.json());
|
|
98
|
+
if (!data.session_id)
|
|
99
|
+
throw new Error('join response missing session_id');
|
|
100
|
+
this.sessionId = data.session_id;
|
|
101
|
+
logger.flow('remote-phone: joined channel', { channelId: this.channelId, callsign: this.callsign });
|
|
102
|
+
}
|
|
103
|
+
startReceiving() {
|
|
104
|
+
if (this.receiving)
|
|
105
|
+
return;
|
|
106
|
+
this.receiving = true;
|
|
107
|
+
this.abort = new AbortController();
|
|
108
|
+
void this.receiveLoop();
|
|
109
|
+
}
|
|
110
|
+
async receiveLoop() {
|
|
111
|
+
let backoff = 1000;
|
|
112
|
+
while (this.receiving) {
|
|
113
|
+
try {
|
|
114
|
+
const url = `${this.origin}/api/channels/${this.channelId}/wait?timeout=120` +
|
|
115
|
+
(this.lastSeenId ? `&since=${this.lastSeenId}` : '');
|
|
116
|
+
const res = await fetch(url, {
|
|
117
|
+
headers: {
|
|
118
|
+
'Authorization': `Bearer ${this.channelToken}`,
|
|
119
|
+
'X-Session-Id': this.sessionId,
|
|
120
|
+
},
|
|
121
|
+
signal: this.abort?.signal,
|
|
122
|
+
});
|
|
123
|
+
if (res.status === 401 || res.status === 410) {
|
|
124
|
+
logger.flow('remote-phone: session expired, rejoining');
|
|
125
|
+
await this.join();
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
const body = await res.text().catch(() => '');
|
|
130
|
+
throw new Error(`wait failed: HTTP ${res.status} ${body.slice(0, 160)}`);
|
|
131
|
+
}
|
|
132
|
+
const data = (await res.json());
|
|
133
|
+
backoff = 1000;
|
|
134
|
+
for (const m of data.messages || []) {
|
|
135
|
+
if (m.id > this.lastSeenId)
|
|
136
|
+
this.lastSeenId = m.id;
|
|
137
|
+
if (m.kind === 'status')
|
|
138
|
+
continue;
|
|
139
|
+
const msg = { id: m.id, from: m.from, text: m.text, at: m.at };
|
|
140
|
+
for (const cb of this.messageListeners) {
|
|
141
|
+
try {
|
|
142
|
+
cb(msg);
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
logger.warn(`remote-phone message listener error: ${e}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
if (!this.receiving)
|
|
152
|
+
break;
|
|
153
|
+
const err = e;
|
|
154
|
+
if (err.name === 'AbortError')
|
|
155
|
+
break;
|
|
156
|
+
logger.warn(`remote-phone receive loop error: ${err.message}`);
|
|
157
|
+
this.notice(`๐ฑ Remote phone: reconnecting (${err.message})`);
|
|
158
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
159
|
+
backoff = Math.min(backoff * 2, 30000);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
logger.flow('remote-phone: receive loop ended');
|
|
163
|
+
}
|
|
164
|
+
async postReply(text) {
|
|
165
|
+
if (!this.isActive() || !text.trim())
|
|
166
|
+
return;
|
|
167
|
+
const trimmed = text.trim();
|
|
168
|
+
if (trimmed === this.lastPostedReply)
|
|
169
|
+
return;
|
|
170
|
+
this.lastPostedReply = trimmed;
|
|
171
|
+
await this.send(trimmed.slice(0, 8000));
|
|
172
|
+
}
|
|
173
|
+
async sendStatus(text) {
|
|
174
|
+
if (!this.isActive())
|
|
175
|
+
return;
|
|
176
|
+
await this.send(text.slice(0, 280), 'status');
|
|
177
|
+
}
|
|
178
|
+
async send(message, kind) {
|
|
179
|
+
try {
|
|
180
|
+
await fetch(`${this.origin}/api/channels/${this.channelId}/send`, {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
headers: {
|
|
183
|
+
'Authorization': `Bearer ${this.channelToken}`,
|
|
184
|
+
'X-Session-Id': this.sessionId,
|
|
185
|
+
'Content-Type': 'application/json',
|
|
186
|
+
},
|
|
187
|
+
body: JSON.stringify({ to: 'all', message, ...(kind ? { kind } : {}) }),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
logger.warn(`remote-phone send failed: ${e.message}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async close() {
|
|
195
|
+
if (!this.channelId)
|
|
196
|
+
return;
|
|
197
|
+
this.receiving = false;
|
|
198
|
+
this.abort?.abort();
|
|
199
|
+
const id = this.channelId, token = this.channelToken, sid = this.sessionId;
|
|
200
|
+
this.channelId = this.channelToken = this.sessionId = null;
|
|
201
|
+
this.callsign = this.identityKey = this.ownerPassword = null;
|
|
202
|
+
this.cachedHandoff = null;
|
|
203
|
+
this.notice('๐ฑ Remote phone channel closed.');
|
|
204
|
+
try {
|
|
205
|
+
await fetch(`${this.origin}/api/channels/${id}/leave`, {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
headers: { 'Authorization': `Bearer ${token}`, 'X-Session-Id': sid },
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
catch { }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
export const remotePhone = new RemotePhoneController();
|
|
214
|
+
//# sourceMappingURL=remote-phone.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { logger } from '../../../utils/logger.js';
|
|
2
|
+
import { remotePhone } from '../../../orquesta/remote-phone.js';
|
|
3
|
+
const DEFINITION = {
|
|
4
|
+
type: 'function',
|
|
5
|
+
function: {
|
|
6
|
+
name: 'open_remote_phone_channel',
|
|
7
|
+
description: 'Open a RogerThat remote-control channel so the user can drive this session from their phone. ' +
|
|
8
|
+
'Use this when the user asks to "drive you from my phone", "open a remote channel", or set up ' +
|
|
9
|
+
'phone/mobile control. It mints the channel, starts listening for phone messages in-process, and ' +
|
|
10
|
+
'returns a handoff block with the link and PIN. After calling it, relay the returned operator_handoff ' +
|
|
11
|
+
'text to the user verbatim (it contains both the one-tap link and the PIN-protected link). ' +
|
|
12
|
+
'Do NOT use curl, npx rogerthat, or background tail/waiter loops โ this tool handles all of that.',
|
|
13
|
+
parameters: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
reason: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: 'Why you are opening the channel (shown to the user).',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
required: [],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
async function execute() {
|
|
26
|
+
logger.enter('open_remote_phone_channel');
|
|
27
|
+
try {
|
|
28
|
+
const handoff = await remotePhone.open();
|
|
29
|
+
const result = [
|
|
30
|
+
'Remote phone channel is LIVE. Relay this to the user verbatim:',
|
|
31
|
+
'',
|
|
32
|
+
handoff.operatorHandoff,
|
|
33
|
+
'',
|
|
34
|
+
`Channel: ${handoff.channelId} ยท PIN: ${handoff.pin} ยท valid 24h.`,
|
|
35
|
+
'You are now listening for their phone messages and will reply to the channel automatically.',
|
|
36
|
+
].join('\n');
|
|
37
|
+
return { success: true, result, metadata: { channelId: handoff.channelId } };
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
const err = e;
|
|
41
|
+
logger.warn(`open_remote_phone_channel failed: ${err.message}`);
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
error: `Could not open the remote phone channel: ${err.message}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export const RemotePhoneTool = {
|
|
49
|
+
definition: DEFINITION,
|
|
50
|
+
execute,
|
|
51
|
+
categories: ['llm-simple'],
|
|
52
|
+
description: 'Open a RogerThat remote-control channel for phone-driven control',
|
|
53
|
+
};
|
|
54
|
+
export default RemotePhoneTool;
|
|
55
|
+
//# sourceMappingURL=remote-phone-tool.js.map
|
package/dist/tools/registry.js
CHANGED
|
@@ -7,6 +7,7 @@ import { USER_INTERACTION_TOOLS } from './llm/simple/user-interaction-tools.js';
|
|
|
7
7
|
import { TODO_TOOLS } from './llm/simple/todo-tools.js';
|
|
8
8
|
import { PLANNING_TOOLS } from './llm/simple/planning-tools.js';
|
|
9
9
|
import { FinalResponseTool } from './llm/simple/final-response-tool.js';
|
|
10
|
+
import { RemotePhoneTool } from './llm/simple/remote-phone-tool.js';
|
|
10
11
|
import { getShellTools } from './llm/simple/index.js';
|
|
11
12
|
import { BROWSER_TOOLS, startBrowserServer, shutdownBrowserServer } from './browser/index.js';
|
|
12
13
|
import { WORD_TOOLS, EXCEL_TOOLS, POWERPOINT_TOOLS } from './office/index.js';
|
|
@@ -261,6 +262,7 @@ export function initializeToolRegistry() {
|
|
|
261
262
|
toolRegistry.registerAll(shellTools);
|
|
262
263
|
toolRegistry.registerAll(TODO_TOOLS);
|
|
263
264
|
toolRegistry.register(FinalResponseTool);
|
|
265
|
+
toolRegistry.register(RemotePhoneTool);
|
|
264
266
|
toolRegistry.registerAll(PLANNING_TOOLS);
|
|
265
267
|
}
|
|
266
268
|
export async function initializeOptionalTools() {
|
|
@@ -35,6 +35,7 @@ import { executeSlashCommand, isSlashCommand, } from '../../core/slash-command-h
|
|
|
35
35
|
import { closeJsonStreamLogger } from '../../utils/json-stream-logger.js';
|
|
36
36
|
import { configManager } from '../../core/config/config-manager.js';
|
|
37
37
|
import { readHookConfig } from '../../orquesta/hook-init.js';
|
|
38
|
+
import { remotePhone } from '../../orquesta/remote-phone.js';
|
|
38
39
|
import { logger } from '../../utils/logger.js';
|
|
39
40
|
import { usageTracker } from '../../core/usage-tracker.js';
|
|
40
41
|
import { UpdateNotification } from '../UpdateNotification.js';
|
|
@@ -150,6 +151,9 @@ export const PlanExecuteApp = ({ llmClient: initialLlmClient, modelInfo }) => {
|
|
|
150
151
|
}), []);
|
|
151
152
|
const lastCtrlCTimeRef = React.useRef(0);
|
|
152
153
|
const DOUBLE_TAP_THRESHOLD = 1500;
|
|
154
|
+
const handleSubmitRef = React.useRef(() => { });
|
|
155
|
+
const lastPhonePostedIdxRef = React.useRef(-1);
|
|
156
|
+
const phoneWasActiveRef = React.useRef(false);
|
|
153
157
|
const addLog = useCallback((entry) => {
|
|
154
158
|
if (!entry.content || entry.content.trim() === '') {
|
|
155
159
|
return;
|
|
@@ -475,6 +479,8 @@ export const PlanExecuteApp = ({ llmClient: initialLlmClient, modelInfo }) => {
|
|
|
475
479
|
}, []);
|
|
476
480
|
const handleExit = useCallback(async () => {
|
|
477
481
|
logger.flow('Exiting application');
|
|
482
|
+
if (remotePhone.isActive())
|
|
483
|
+
await remotePhone.close().catch(() => { });
|
|
478
484
|
await closeJsonStreamLogger();
|
|
479
485
|
exit();
|
|
480
486
|
}, [exit]);
|
|
@@ -990,6 +996,49 @@ export const PlanExecuteApp = ({ llmClient: initialLlmClient, modelInfo }) => {
|
|
|
990
996
|
handleExit,
|
|
991
997
|
addLog,
|
|
992
998
|
]);
|
|
999
|
+
useEffect(() => {
|
|
1000
|
+
handleSubmitRef.current = handleSubmit;
|
|
1001
|
+
}, [handleSubmit]);
|
|
1002
|
+
useEffect(() => {
|
|
1003
|
+
const offMessage = remotePhone.onMessage((msg) => {
|
|
1004
|
+
logger.flow('remote-phone: injecting phone message as user turn', { from: msg.from });
|
|
1005
|
+
addLog({ type: 'user_input', content: `๐ฑ ${msg.text}` });
|
|
1006
|
+
void remotePhone.sendStatus('on it');
|
|
1007
|
+
handleSubmitRef.current(msg.text);
|
|
1008
|
+
});
|
|
1009
|
+
const offNotice = remotePhone.onNotice((text) => {
|
|
1010
|
+
addLog({ type: 'assistant_message', content: text });
|
|
1011
|
+
});
|
|
1012
|
+
return () => { offMessage(); offNotice(); };
|
|
1013
|
+
}, [addLog]);
|
|
1014
|
+
useEffect(() => {
|
|
1015
|
+
const active = remotePhone.isActive();
|
|
1016
|
+
if (active && !phoneWasActiveRef.current) {
|
|
1017
|
+
phoneWasActiveRef.current = true;
|
|
1018
|
+
lastPhonePostedIdxRef.current = messages.length - 1;
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
if (!active) {
|
|
1022
|
+
phoneWasActiveRef.current = false;
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
if (isProcessing)
|
|
1026
|
+
return;
|
|
1027
|
+
let lastAssistantIdx = -1;
|
|
1028
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1029
|
+
if (messages[i]?.role === 'assistant') {
|
|
1030
|
+
lastAssistantIdx = i;
|
|
1031
|
+
break;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
if (lastAssistantIdx <= lastPhonePostedIdxRef.current)
|
|
1035
|
+
return;
|
|
1036
|
+
const content = messages[lastAssistantIdx]?.content;
|
|
1037
|
+
if (content && content.trim()) {
|
|
1038
|
+
lastPhonePostedIdxRef.current = lastAssistantIdx;
|
|
1039
|
+
void remotePhone.postReply(content);
|
|
1040
|
+
}
|
|
1041
|
+
}, [messages, isProcessing]);
|
|
993
1042
|
useEffect(() => {
|
|
994
1043
|
if (!isProcessing && pendingUserMessage && llmClient) {
|
|
995
1044
|
logger.flow('Processing remaining pending user message after execution complete');
|
|
@@ -61,6 +61,11 @@ export const SLASH_COMMANDS = [
|
|
|
61
61
|
name: '/hook',
|
|
62
62
|
description: 'Claude Code hook here: status | enable | disable',
|
|
63
63
|
},
|
|
64
|
+
{
|
|
65
|
+
name: '/remote-phone',
|
|
66
|
+
description: 'Drive this session from your phone (RogerThat): on | off | status',
|
|
67
|
+
argsHint: 'on | off | status',
|
|
68
|
+
},
|
|
64
69
|
{
|
|
65
70
|
name: '/update',
|
|
66
71
|
description: 'Update orquesta-cli to the latest version',
|