Skip to content

Commit a2b504e

Browse files
committed
chore(server): fix lint errors in cli, organize imports; build server
1 parent 4c7f3e3 commit a2b504e

File tree

21 files changed

+1180
-198
lines changed

21 files changed

+1180
-198
lines changed

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,38 @@ Flags
7575
--pin <code> Override generated PIN (pair only)
7676
```
7777

78+
Terminal commands (resume sessions on desktop)
79+
----------------------------------------------
80+
81+
Pocket Server can attach your mobile terminal sessions on your desktop terminal. No pairing or token is required on your own machine — the CLI and server exchange a local secret under `~/.pocket-server/data/runtime/local-ws.key`.
82+
83+
```
84+
# List active terminal sessions with indices
85+
pocket-server terminal sessions
86+
87+
# Attach by index (from the list)
88+
pocket-server terminal attach 2
89+
90+
# Attach by title (case-insensitive, supports spaces)
91+
pocket-server terminal attach --name "Opencode"
92+
# or positional query
93+
pocket-server terminal attach "Opencode"
94+
95+
# Attach by id
96+
pocket-server terminal attach --id term:/path#3
97+
98+
# JSON output for tooling
99+
pocket-server terminal sessions --json
100+
101+
# Optional: specify a port if not 3000
102+
pocket-server terminal attach 1 --port 3010
103+
```
104+
105+
Notes
106+
- Attach streams the session interactively into your current terminal. Press Ctrl+C to detach without closing the remote PTY.
107+
- The desktop attach replays terminal output to reconstruct the TUI exactly as you left it on mobile. It does not clear your local terminal.
108+
- Session titles come from the mobile tabs; you can long‑press to rename on mobile and they’ll appear here.
109+
78110
How Pocket works (high level)
79111
-----------------------------
80112

src/agent/anthropic/anthropic.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { generateTitle } from './title';
1010
import { bashToolDefinition, executeBash } from './tools/bash';
1111
import { editorToolDefinition, executeEditor } from './tools/editor';
1212
import { executeWebSearch, webSearchToolDefinition } from './tools/web-search';
13+
import { executeWorkPlan, workPlanToolDefinition } from './tools/work-plan';
1314
import type {
1415
AgentSession,
1516
ClientMessage,
@@ -63,7 +64,7 @@ export class AnthropicService {
6364
},
6465
settings: {
6566
maxTokens: 4096,
66-
tools: [bashToolDefinition, editorToolDefinition, webSearchToolDefinition]
67+
tools: [bashToolDefinition, editorToolDefinition, webSearchToolDefinition, workPlanToolDefinition]
6768
}
6869
},
6970
streamingState: {
@@ -137,7 +138,7 @@ export class AnthropicService {
137138
const systemPrompt = generateSystemPrompt({ workingDirectory: workingDir });
138139

139140
// Prepare tools
140-
const tools = [bashToolDefinition, editorToolDefinition, webSearchToolDefinition];
141+
const tools = [bashToolDefinition, editorToolDefinition, webSearchToolDefinition, workPlanToolDefinition];
141142

142143
try {
143144
// Store reference to current stream for potential cancellation
@@ -239,6 +240,10 @@ export class AnthropicService {
239240
output = await executeWebSearch(req.input, workingDir);
240241
isError = false;
241242
break;
243+
case 'work_plan':
244+
output = await executeWorkPlan(sessionId, req.input);
245+
isError = false;
246+
break;
242247
default:
243248
output = `Unknown tool: ${req.name}`;
244249
isError = true;
@@ -386,6 +391,10 @@ export class AnthropicService {
386391
result = await executeWebSearch(toolUse.input, session.workingDir);
387392
isError = false;
388393
break;
394+
case 'work_plan':
395+
result = await executeWorkPlan(session.id, toolUse.input);
396+
isError = false;
397+
break;
389398
default:
390399
result = `Unknown tool: ${toolUse.name}`;
391400
isError = true;
@@ -484,7 +493,7 @@ export class AnthropicService {
484493
onMessage: (msg: ServerMessage) => void
485494
): Promise<void> {
486495
const systemPrompt = generateSystemPrompt({ workingDirectory: session.workingDir });
487-
const tools = [bashToolDefinition, editorToolDefinition, webSearchToolDefinition];
496+
const tools = [bashToolDefinition, editorToolDefinition, webSearchToolDefinition, workPlanToolDefinition];
488497

489498
try {
490499
// Store reference to current stream for potential cancellation
@@ -566,6 +575,10 @@ export class AnthropicService {
566575
output = await executeWebSearch(req.input, session.workingDir);
567576
isError = false;
568577
break;
578+
case 'work_plan':
579+
output = await executeWorkPlan(session.id, req.input);
580+
isError = false;
581+
break;
569582
default:
570583
output = `Unknown tool: ${req.name}`;
571584
isError = true;
@@ -753,4 +766,4 @@ export class AnthropicService {
753766
}
754767

755768
// Export singleton instance
756-
export const anthropicService = new AnthropicService();
769+
export const anthropicService = new AnthropicService();

src/agent/anthropic/prompt.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,17 @@ Search the web for current information:
6868
- Up to 5 searches per request
6969
- Results include page titles, URLs, and content
7070
71+
### 4. Work Plan Tool (work_plan)
72+
Use a structured to-do for multi-step tasks:
73+
- **create**: Declare an ordered list of steps with short titles; include \`estimated_seconds\` when possible.
74+
- **complete**: Mark a step as done by id when you finish it.
75+
- **revise**: Adjust titles, order, or add/remove items if your plan changes.
76+
77+
Guidelines:
78+
- Only create a plan for genuinely multi-step requests. For a simple greeting or single-step ask, skip the plan.
79+
- Keep step titles short and mobile-friendly (<= 80 chars).
80+
- When a step is finished, call \`complete\` immediately so the user receives a push about the next step.
81+
7182
## Operating Modes
7283
7384
### Standard Mode
@@ -104,4 +115,4 @@ When enabled by the user:
104115
- Consider error handling and edge cases
105116
106117
Remember: You're helping users code effectively from their mobile devices, making development accessible anywhere.`;
107-
}
118+
}

src/agent/anthropic/streaming.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,8 @@ export async function processStream(
192192

193193
// For Max (chatMode === false) auto-approval: queue tool requests to execute AFTER this stream completes,
194194
// so we can send them back to the API in the required "user tool_result" message format.
195-
if (!chatMode && isToolSafe(toolRequest)) {
195+
// Auto-approve safe tools in Max mode, and always auto-approve work_plan
196+
if ((!chatMode && isToolSafe(toolRequest)) || toolRequest.name === 'work_plan') {
196197
state.autoToolRequests?.push(toolRequest);
197198
} else {
198199
// Chat mode: send to client for manual approval
@@ -335,6 +336,8 @@ function isToolSafe(request: ToolRequest): boolean {
335336
return !isEditorCommandDangerous(request.input);
336337
case 'web_search':
337338
return true; // Web search is always safe
339+
case 'work_plan':
340+
return true; // Server-local plan state; safe
338341
default:
339342
return false;
340343
}
@@ -365,7 +368,17 @@ function generateToolDescription(toolName: string, input: any): string {
365368
}
366369
} else if (toolName === 'web_search' && input?.query) {
367370
return `Search: ${input.query}`;
371+
} else if (toolName === 'work_plan') {
372+
if (input?.command === 'create' && Array.isArray(input?.items)) {
373+
return `Create work plan: ${input.items.length} steps`;
374+
}
375+
if (input?.command === 'complete') {
376+
return `Complete step ${input.id}`;
377+
}
378+
if (input?.command === 'revise') {
379+
return `Revise work plan (${(input.items || []).length} changes)`;
380+
}
368381
}
369382

370383
return 'Execute tool';
371-
}
384+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* Work Plan Tool
3+
* Lets the model declare a multi-step plan and mark steps complete.
4+
* We persist plan state in the session snapshot and emit targeted push notifications
5+
* via the existing Expo notifications pipeline.
6+
*/
7+
8+
import { notificationsService } from '../../../notifications/index';
9+
import { getInitiatorDeviceId } from '../../session-initiators';
10+
import { sessionStoreFs } from '../../store/session-store-fs';
11+
import type { WorkPlanCommand, WorkPlanTool } from '../types';
12+
13+
export const workPlanToolDefinition: WorkPlanTool = {
14+
type: 'work_plan_20250828',
15+
name: 'work_plan',
16+
};
17+
18+
/**
19+
* Execute work plan command. This mutates server-side plan state for the session.
20+
* Returns a short, user-readable summary used in the tool_result block.
21+
*/
22+
export async function executeWorkPlan(
23+
sessionId: string,
24+
input: WorkPlanCommand,
25+
): Promise<string> {
26+
switch (input.command) {
27+
case 'create': {
28+
const items = (input.items || []).slice().sort((a, b) => a.order - b.order);
29+
if (items.length === 0) {
30+
return 'No work plan items provided';
31+
}
32+
await sessionStoreFs.recordWorkPlanCreate(sessionId, items);
33+
// Notify: show the first current task
34+
const snap = await sessionStoreFs.getSnapshot(sessionId);
35+
const title = snap?.title || 'Agent';
36+
const total = items.length;
37+
const first = items[0]?.title || 'Step 1';
38+
const deviceId = snap?.initiatorDeviceId || getInitiatorDeviceId(sessionId);
39+
if (deviceId) {
40+
await notificationsService.notifyAgentPlanProgress({
41+
deviceId,
42+
sessionId,
43+
sessionTitle: title,
44+
kind: 'created',
45+
stepIndex: 1,
46+
total,
47+
taskTitle: first,
48+
});
49+
}
50+
return `Work plan created with ${total} steps.`;
51+
}
52+
53+
case 'complete': {
54+
const res = await sessionStoreFs.recordWorkPlanComplete(sessionId, input.id);
55+
if (!res) {
56+
return `No matching step found for id ${input.id}`;
57+
}
58+
const snap = await sessionStoreFs.getSnapshot(sessionId);
59+
const title = snap?.title || 'Agent';
60+
const deviceId = snap?.initiatorDeviceId || getInitiatorDeviceId(sessionId);
61+
if (deviceId) {
62+
await notificationsService.notifyAgentPlanProgress({
63+
deviceId,
64+
sessionId,
65+
sessionTitle: title,
66+
kind: res.next ? 'next' : 'completed',
67+
stepIndex: res.next ? res.completed + 1 : res.total,
68+
total: res.total,
69+
taskTitle: res.next?.title || 'All steps completed',
70+
});
71+
}
72+
if (res.next) {
73+
return `Completed step "${res.completedItem.title}". Next: (${res.completed + 1}/${res.total}) "${res.next.title}".`;
74+
}
75+
return `Completed step "${res.completedItem.title}". Plan finished (${res.total}/${res.total}).`;
76+
}
77+
78+
case 'revise': {
79+
const res = await sessionStoreFs.recordWorkPlanRevise(sessionId, input.items || []);
80+
const count = res?.total ?? 0;
81+
return `Revised work plan. Now ${count} steps.`;
82+
}
83+
84+
default:
85+
return `Unknown work_plan command`;
86+
}
87+
}

src/agent/anthropic/types.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,43 @@ export interface TextEditorTool extends SpecialTool {
229229
name: 'str_replace_based_edit_tool';
230230
}
231231

232+
// Work Plan Tool
233+
export interface WorkPlanTool extends SpecialTool {
234+
type: 'work_plan_20250828';
235+
name: 'work_plan';
236+
}
237+
238+
export type WorkPlanCommand =
239+
| WorkPlanCreateCommand
240+
| WorkPlanCompleteCommand
241+
| WorkPlanReviseCommand;
242+
243+
export interface WorkPlanCreateCommand {
244+
command: 'create';
245+
items: Array<{
246+
id: string;
247+
title: string;
248+
order: number;
249+
estimated_seconds?: number;
250+
}>;
251+
}
252+
253+
export interface WorkPlanCompleteCommand {
254+
command: 'complete';
255+
id: string;
256+
}
257+
258+
export interface WorkPlanReviseCommand {
259+
command: 'revise';
260+
items: Array<{
261+
id: string;
262+
title?: string;
263+
order?: number;
264+
estimated_seconds?: number;
265+
remove?: boolean;
266+
}>;
267+
}
268+
232269
export type TextEditorCommand =
233270
| ViewCommand
234271
| StrReplaceCommand
@@ -482,4 +519,4 @@ export interface ToolOutput {
482519
output: string;
483520
isError: boolean;
484521
input?: any; // Optional, for context
485-
}
522+
}

src/agent/index.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
import { verifyAuthFromRequest } from '../auth/middleware';
77
import type { Router } from '../server/router';
8+
import { wsManager } from '../server/websocket';
89
import { handleAgentWebSocket, registerAgentRoutes } from './anthropic/index';
10+
import { setInitiatorDeviceId } from './session-initiators';
911
import { sessionStoreFs } from './store/session-store-fs';
1012

1113
/**
@@ -41,6 +43,18 @@ export async function handleAgentMessage(
4143
): Promise<void> {
4244
// Route based on message type prefix
4345
if (message.type?.startsWith('agent:')) {
46+
// Capture initiator device for this session (for targeted pushes)
47+
try {
48+
if (message.type === 'agent:message' && typeof message.sessionId === 'string') {
49+
const client = wsManager.getClient(clientId);
50+
const deviceId = (client?.metadata as any)?.deviceId as string | undefined;
51+
if (deviceId) {
52+
setInitiatorDeviceId(message.sessionId, deviceId);
53+
// Persist if snapshot already exists
54+
void sessionStoreFs.setInitiator(message.sessionId, deviceId);
55+
}
56+
}
57+
} catch {}
4458
// Default to Anthropic for now
4559
await handleAgentWebSocket(ws, clientId, message);
4660
}
@@ -59,4 +73,4 @@ export type {
5973
ServerMessage,
6074
ToolOutput,
6175
ToolRequest
62-
} from './anthropic/types';
76+
} from './anthropic/types';

src/agent/session-initiators.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Simple in-memory mapping of agent session -> initiating deviceId.
3+
* This complements the persisted snapshot's initiatorDeviceId, providing
4+
* an early mapping before the snapshot is created.
5+
*/
6+
7+
const initiators = new Map<string, string>();
8+
9+
export function setInitiatorDeviceId(sessionId: string, deviceId: string): void {
10+
if (!sessionId || !deviceId) return;
11+
if (!initiators.has(sessionId)) {
12+
initiators.set(sessionId, deviceId);
13+
}
14+
}
15+
16+
export function getInitiatorDeviceId(sessionId: string): string | undefined {
17+
return initiators.get(sessionId);
18+
}
19+

0 commit comments

Comments
 (0)