@mjasnikovs/pi-task 0.2.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -64,6 +64,7 @@ pi install npm:@mjasnikovs/pi-task
64
64
  | `/task-auto <feature>` | Plan a feature into a task list and run each title through `/task` in order (resumable). |
65
65
  | `/task-auto-resume` | Resume the active `/task-auto` run at the next unfinished task. |
66
66
  | `/task-auto-cancel` | Stop the `/task-auto` loop after the current task (still resumable). |
67
+ | `/remote` | Show the QR code & URLs for the always-on web view (`/remote stop` to stop). Answer grill questions, start tasks, and watch progress from your phone. |
67
68
 
68
69
  ## The pipeline
69
70
 
@@ -103,6 +104,18 @@ A real feature is usually several tasks, not one. `/task-auto` is a thin planner
103
104
  - **Sequential, blocking.** Each title runs through `/task` to a spec, the spec is implemented, and the loop waits for that to finish before starting the next title. No overlap.
104
105
  - **Crash- and cancel-safe.** Progress is the markdown checkboxes in the AUTO file. `/task-auto-resume` (no id) automatically picks up the active run at the first unchecked title. If a title's `/task` run fails, the loop stops and leaves the run resumable.
105
106
 
107
+ ## Remote — drive a task from your phone
108
+
109
+ The remote server is **always on** — it starts automatically with each session, with nothing taking up screen space. Run `/remote` any time to pop a QR code and the connection URLs: a **Tailscale** line and a **LAN** line when both are available (the QR encodes the Tailscale-preferred one). Open the URL on any device that can reach the host and you get a live view of the session: streaming output, tool calls, and the `/task` status block (phase, elapsed, context). It's bidirectional — the browser can:
110
+
111
+ - **Answer grill / `/task-auto` clarify questions.** Each question appears as a card with the recommended default pre-filled (Accept), a free-text box (Submit), Skip, or Cancel task.
112
+ - **Start and control tasks.** Type `/task …`, `/task-auto …`, `/task-cancel`, `/task-resume`, etc. — they run on the host.
113
+ - **Send plain messages** to the agent.
114
+
115
+ Prompts use a **first-answer-wins race**: the same question shows in the local TUI *and* every connected browser, and whoever answers first wins — the other surfaces dismiss. With nobody connected, `/task` behaves exactly as before; the remote path is purely additive.
116
+
117
+ `/remote stop` shuts the server down for the rest of the session (it comes back on the next session start). There is **no authentication** — it's a personal LAN/Tailscale tool. Don't expose the port to untrusted networks.
118
+
106
119
  ## Bundled tools
107
120
 
108
121
  `pi-task` also registers four MCP-style worker tools (formerly `@mjasnikovs/pi-worker`). All are parallel-execution-capable, so the parent session can issue several calls in one turn.
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { registerTask } from './task/orchestrator.js';
2
2
  import { registerTaskAuto } from './task/auto-orchestrator.js';
3
3
  import { registerWorkers } from './workers/index.js';
4
+ import { registerRemote } from './remote/register.js';
4
5
  export default function (pi) {
5
6
  registerTask(pi);
6
7
  registerTaskAuto(pi);
7
8
  registerWorkers(pi);
9
+ registerRemote(pi);
8
10
  }
@@ -0,0 +1,78 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
2
+ import type { PromptMessage, ServerMessage } from './protocol.js';
3
+ export interface BridgeState {
4
+ /** promptId → resolver that settles the remote side of an ask() race. */
5
+ pending: Map<string, (value: string | undefined) => void>;
6
+ /** The prompt currently awaiting an answer (replayed to late joiners), or null. */
7
+ activePrompt: PromptMessage | null;
8
+ /** Last lines pushed per widget key (replayed to late joiners). */
9
+ activeWidgets: Map<string, string[]>;
10
+ nextId: number;
11
+ /** Command name → handler, populated as pi-task registers its commands. */
12
+ commands: Map<string, (args: string, ctx: ExtensionCommandContext) => unknown>;
13
+ /** Most recent live command context, for remote-initiated dispatch. */
14
+ currentCtx: ExtensionCommandContext | null;
15
+ /** Broadcast sink — swapped in tests. */
16
+ broadcast: (msg: ServerMessage) => void;
17
+ /** @internal Test-only capture of broadcast messages; empty in production. */
18
+ sent: ServerMessage[];
19
+ }
20
+ export declare function getBridge(): BridgeState;
21
+ /** Resolve the remote side of a pending prompt. First call wins; later calls
22
+ * (duplicate frames, a second browser, local-after-remote) are ignored. */
23
+ export declare function answerPrompt(id: string, value: string | undefined): void;
24
+ export interface AskSpec {
25
+ /** Themed, possibly multi-line title for the local TUI input. */
26
+ localTitle: string;
27
+ /** Plain question text for the browser card. */
28
+ question: string;
29
+ /** Plain recommended default (prefilled in both surfaces), if any. */
30
+ recommended?: string;
31
+ /** Whether the browser card shows a Skip button (answers with empty string). */
32
+ allowSkip: boolean;
33
+ }
34
+ /** Wraps a live command ctx and fans interactions out to local TUI + browsers. */
35
+ export declare class SessionUI {
36
+ private readonly ctx;
37
+ private readonly bridge;
38
+ constructor(ctx: ExtensionCommandContext, bridge?: BridgeState);
39
+ get theme(): ExtensionCommandContext['ui']['theme'];
40
+ get hasUI(): boolean;
41
+ /** Race the local input against a remote answer; first to settle wins. */
42
+ ask(spec: AskSpec): Promise<string | undefined>;
43
+ }
44
+ /** Mirror a status widget to browsers and remember it for late joiners.
45
+ * `lines === undefined` clears the widget (broadcast as `lines: null`). */
46
+ export declare function publishWidget(key: string, lines: string[] | undefined): void;
47
+ export declare function publishNotify(message: string, level: 'info' | 'warning' | 'error'): void;
48
+ export declare function publishViewer(title: string, text: string): void;
49
+ interface BridgeCommandDef {
50
+ description: string;
51
+ handler: (args: string, ctx: ExtensionCommandContext) => unknown;
52
+ [k: string]: unknown;
53
+ }
54
+ /** Register a command with pi AND record it in the bridge so remote slash lines
55
+ * can invoke it. Use in place of pi.registerCommand for task commands. */
56
+ export declare function registerBridgeCommand(pi: ExtensionAPI, name: string, def: BridgeCommandDef): void;
57
+ /** Start a new session in response to a remote `/new`. Reads the freshest
58
+ * command-capable ctx (currentCtx) at call time and mirrors dispatchRemoteLine's
59
+ * guards: a missing, non-command, or stale ctx toasts instead of crashing.
60
+ *
61
+ * The crash this prevents: a captured ctx goes stale after a *local* /new (a
62
+ * replacement we don't initiate, so we never get a withSession to rebind), and
63
+ * ctx.newSession() then throws *synchronously* from assertActive — a sync throw
64
+ * that a bare `.catch()` on the result would miss. `rebind` runs as withSession
65
+ * with the fresh post-replacement ctx. */
66
+ /** The richer context the runtime passes to a `newSession` withSession callback
67
+ * (it carries sendUserMessage, unlike a plain command ctx). Derived from the
68
+ * newSession signature since the package root doesn't re-export the type. */
69
+ type NewSessionOptions = NonNullable<Parameters<ExtensionCommandContext['newSession']>[0]>;
70
+ type ReplacedSessionContext = Parameters<NonNullable<NewSessionOptions['withSession']>>[0];
71
+ export declare function dispatchRemoteNewSession(rebind: (ctx: ReplacedSessionContext) => void): void;
72
+ /** Handle one line typed in a browser. Returns true if it was consumed as a
73
+ * slash command (registered or unknown); false if it's a plain chat line that
74
+ * the caller should forward via onPlain. */
75
+ export declare function dispatchRemoteLine(text: string, opts: {
76
+ onPlain: (text: string) => void;
77
+ }): boolean;
78
+ export {};
@@ -0,0 +1,184 @@
1
+ import { broadcast as wsBroadcast } from './broadcast.js';
2
+ const g = globalThis;
3
+ export function getBridge() {
4
+ if (!g.__piBridge) {
5
+ g.__piBridge = {
6
+ pending: new Map(),
7
+ activePrompt: null,
8
+ activeWidgets: new Map(),
9
+ nextId: 0,
10
+ commands: new Map(),
11
+ currentCtx: null,
12
+ broadcast: (msg) => wsBroadcast(msg),
13
+ sent: []
14
+ };
15
+ }
16
+ return g.__piBridge;
17
+ }
18
+ /** Resolve the remote side of a pending prompt. First call wins; later calls
19
+ * (duplicate frames, a second browser, local-after-remote) are ignored. */
20
+ export function answerPrompt(id, value) {
21
+ const b = getBridge();
22
+ const settle = b.pending.get(id);
23
+ if (!settle)
24
+ return;
25
+ b.pending.delete(id);
26
+ settle(value);
27
+ }
28
+ /** Wraps a live command ctx and fans interactions out to local TUI + browsers. */
29
+ export class SessionUI {
30
+ ctx;
31
+ bridge;
32
+ constructor(ctx, bridge = getBridge()) {
33
+ this.ctx = ctx;
34
+ this.bridge = bridge;
35
+ }
36
+ get theme() {
37
+ return this.ctx.ui.theme;
38
+ }
39
+ get hasUI() {
40
+ return this.ctx.hasUI;
41
+ }
42
+ /** Race the local input against a remote answer; first to settle wins. */
43
+ async ask(spec) {
44
+ const b = this.bridge;
45
+ const id = String(b.nextId++);
46
+ const ac = new AbortController();
47
+ const remote = new Promise(resolve => {
48
+ b.pending.set(id, resolve);
49
+ });
50
+ const prompt = {
51
+ type: 'prompt',
52
+ id,
53
+ question: spec.question,
54
+ recommended: spec.recommended,
55
+ allowSkip: spec.allowSkip
56
+ };
57
+ b.activePrompt = prompt;
58
+ b.broadcast(prompt);
59
+ // Local: resolves to a value/undefined, or undefined on abort. Swallow
60
+ // the rejection some implementations throw on abort so it never leaks.
61
+ const local = this.ctx.hasUI ?
62
+ this.ctx.ui
63
+ .input(spec.localTitle, spec.recommended, { signal: ac.signal })
64
+ .catch(() => undefined)
65
+ : new Promise(() => { });
66
+ try {
67
+ const winner = await Promise.race([
68
+ local.then(v => ({ from: 'local', v })),
69
+ remote.then(v => ({ from: 'remote', v }))
70
+ ]);
71
+ if (winner.from === 'remote')
72
+ ac.abort();
73
+ return winner.v;
74
+ }
75
+ finally {
76
+ b.pending.delete(id);
77
+ b.activePrompt = null;
78
+ b.broadcast({ type: 'prompt_resolved', id });
79
+ }
80
+ }
81
+ }
82
+ /** Mirror a status widget to browsers and remember it for late joiners.
83
+ * `lines === undefined` clears the widget (broadcast as `lines: null`). */
84
+ export function publishWidget(key, lines) {
85
+ const b = getBridge();
86
+ if (lines === undefined) {
87
+ b.activeWidgets.delete(key);
88
+ b.broadcast({ type: 'widget', key, lines: null });
89
+ return;
90
+ }
91
+ b.activeWidgets.set(key, lines);
92
+ b.broadcast({ type: 'widget', key, lines });
93
+ }
94
+ export function publishNotify(message, level) {
95
+ getBridge().broadcast({ type: 'notify', message, level });
96
+ }
97
+ export function publishViewer(title, text) {
98
+ getBridge().broadcast({ type: 'viewer', title, text });
99
+ }
100
+ /** Register a command with pi AND record it in the bridge so remote slash lines
101
+ * can invoke it. Use in place of pi.registerCommand for task commands. */
102
+ export function registerBridgeCommand(pi, name, def) {
103
+ const b = getBridge();
104
+ const wrapped = {
105
+ ...def,
106
+ handler: (args, ctx) => {
107
+ b.currentCtx = ctx; // keep latest live ctx for remote dispatch
108
+ return def.handler(args, ctx);
109
+ }
110
+ };
111
+ b.commands.set(name, wrapped.handler);
112
+ pi.registerCommand(name, wrapped);
113
+ }
114
+ export function dispatchRemoteNewSession(rebind) {
115
+ const b = getBridge();
116
+ const ctx = b.currentCtx;
117
+ if (!ctx) {
118
+ publishNotify('Start a session before running /new remotely.', 'warning');
119
+ return;
120
+ }
121
+ // A bare event-scoped ctx lacks newSession; a stale command ctx still has it
122
+ // but throws on call — the try/catch below covers that case.
123
+ if (typeof ctx.waitForIdle !== 'function') {
124
+ publishNotify('Session changed locally — run any task command in the terminal once to re-enable remote /new.', 'warning');
125
+ return;
126
+ }
127
+ const toastErr = (err) => publishNotify(`/new failed: ${err.message}`, 'error');
128
+ try {
129
+ const result = ctx.newSession({
130
+ // eslint-disable-next-line @typescript-eslint/require-await
131
+ withSession: async (newCtx) => {
132
+ b.currentCtx = newCtx;
133
+ rebind(newCtx);
134
+ }
135
+ });
136
+ if (result instanceof Promise)
137
+ result.catch(toastErr);
138
+ }
139
+ catch (err) {
140
+ toastErr(err);
141
+ }
142
+ }
143
+ /** Handle one line typed in a browser. Returns true if it was consumed as a
144
+ * slash command (registered or unknown); false if it's a plain chat line that
145
+ * the caller should forward via onPlain. */
146
+ export function dispatchRemoteLine(text, opts) {
147
+ const b = getBridge();
148
+ if (!text.startsWith('/')) {
149
+ opts.onPlain(text);
150
+ return false;
151
+ }
152
+ const space = text.indexOf(' ');
153
+ const name = (space === -1 ? text.slice(1) : text.slice(1, space)).trim();
154
+ const args = space === -1 ? '' : text.slice(space + 1).trim();
155
+ const handler = b.commands.get(name);
156
+ if (!handler) {
157
+ publishNotify(`Unknown command: /${name}`, 'warning');
158
+ return true;
159
+ }
160
+ if (!b.currentCtx) {
161
+ publishNotify('Start a session before running commands remotely.', 'warning');
162
+ return true;
163
+ }
164
+ // Command handlers need a command-capable ctx (waitForIdle/newSession). A bare
165
+ // event-scoped ExtensionContext lacks those; if currentCtx is one (e.g. nothing
166
+ // command-capable has been captured yet), toast instead of throwing a TypeError.
167
+ if (typeof b.currentCtx.waitForIdle !== 'function') {
168
+ publishNotify('Session changed locally — run any task command in the terminal once to re-enable remote commands.', 'warning');
169
+ return true;
170
+ }
171
+ // Invoke synchronously so the call happens immediately, but surface both
172
+ // sync throws and async rejections from the (often async) command handler
173
+ // as a toast instead of crashing or becoming an unhandled rejection.
174
+ const toastErr = (err) => publishNotify(`/${name} failed: ${err.message}`, 'error');
175
+ try {
176
+ const result = handler(args, b.currentCtx);
177
+ if (result instanceof Promise)
178
+ result.catch(toastErr);
179
+ }
180
+ catch (err) {
181
+ toastErr(err);
182
+ }
183
+ return true;
184
+ }
@@ -0,0 +1,6 @@
1
+ import type { WebSocket } from 'ws';
2
+ export declare function addClient(ws: WebSocket): void;
3
+ export declare function removeClient(ws: WebSocket): void;
4
+ export declare function clientCount(): number;
5
+ export declare function broadcast(msg: unknown): void;
6
+ export declare function sendTo(ws: WebSocket, msg: unknown): void;
@@ -0,0 +1,27 @@
1
+ // globalThis persists across jiti module re-evaluations on session switches
2
+ const g = globalThis;
3
+ if (!g.__piRemoteClients)
4
+ g.__piRemoteClients = new Set();
5
+ const clients = g.__piRemoteClients;
6
+ export function addClient(ws) {
7
+ clients.add(ws);
8
+ }
9
+ export function removeClient(ws) {
10
+ clients.delete(ws);
11
+ }
12
+ export function clientCount() {
13
+ return clients.size;
14
+ }
15
+ export function broadcast(msg) {
16
+ const json = JSON.stringify(msg);
17
+ for (const ws of clients) {
18
+ if (ws.readyState === ws.OPEN) {
19
+ ws.send(json);
20
+ }
21
+ }
22
+ }
23
+ export function sendTo(ws, msg) {
24
+ if (ws.readyState === ws.OPEN) {
25
+ ws.send(JSON.stringify(msg));
26
+ }
27
+ }
@@ -0,0 +1,3 @@
1
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
+ import type { HistoryBuffer } from './history.js';
3
+ export declare function setupEvents(pi: ExtensionAPI, history: HistoryBuffer, broadcastFn: (msg: unknown) => void): void;
@@ -0,0 +1,81 @@
1
+ import { setAgentIdle } from './state.js';
2
+ export function setupEvents(pi, history, broadcastFn) {
3
+ let currentText = '';
4
+ const currentTools = [];
5
+ const pendingArgs = new Map();
6
+ pi.on('agent_start', (_event, _ctx) => {
7
+ currentText = '';
8
+ currentTools.length = 0;
9
+ pendingArgs.clear();
10
+ setAgentIdle(false);
11
+ broadcastFn({ type: 'agent_start' });
12
+ });
13
+ pi.on('message_update', (event, _ctx) => {
14
+ const ae = event.assistantMessageEvent;
15
+ if (ae.type === 'text_delta' && 'delta' in ae && typeof ae.delta === 'string') {
16
+ currentText += ae.delta;
17
+ broadcastFn({ type: 'text_delta', delta: ae.delta });
18
+ }
19
+ else if (ae.type === 'error') {
20
+ const errorMessage = 'error' in ae && ae.error && typeof ae.error.errorMessage === 'string' ?
21
+ ae.error.errorMessage
22
+ : '';
23
+ // Skip silent user aborts (no message); only surface genuine failures.
24
+ if (errorMessage || ae.reason === 'error') {
25
+ const message = errorMessage || 'Request failed';
26
+ history.addError(message);
27
+ broadcastFn({ type: 'agent_error', message });
28
+ }
29
+ }
30
+ });
31
+ pi.on('message_end', (_event, _ctx) => {
32
+ broadcastFn({ type: 'text_end' });
33
+ });
34
+ pi.on('tool_execution_start', (event, _ctx) => {
35
+ pendingArgs.set(event.toolCallId, event.args);
36
+ broadcastFn({
37
+ type: 'tool_start',
38
+ toolCallId: event.toolCallId,
39
+ toolName: event.toolName,
40
+ args: event.args
41
+ });
42
+ });
43
+ pi.on('tool_execution_update', (event, _ctx) => {
44
+ broadcastFn({
45
+ type: 'tool_update',
46
+ toolCallId: event.toolCallId,
47
+ partialResult: event.partialResult
48
+ });
49
+ });
50
+ pi.on('tool_execution_end', (event, _ctx) => {
51
+ const args = pendingArgs.get(event.toolCallId);
52
+ pendingArgs.delete(event.toolCallId);
53
+ currentTools.push({
54
+ toolName: event.toolName,
55
+ args,
56
+ result: event.result,
57
+ isError: event.isError
58
+ });
59
+ broadcastFn({
60
+ type: 'tool_end',
61
+ toolCallId: event.toolCallId,
62
+ toolName: event.toolName,
63
+ result: event.result,
64
+ isError: event.isError
65
+ });
66
+ });
67
+ pi.on('agent_end', (_event, ctx) => {
68
+ const contextUsage = ctx.getContextUsage();
69
+ setAgentIdle(true);
70
+ history.addAssistantTurn(currentText, [...currentTools]);
71
+ broadcastFn({ type: 'agent_end', contextUsage });
72
+ currentText = '';
73
+ currentTools.length = 0;
74
+ });
75
+ pi.on('input', (event, _ctx) => {
76
+ if (event.source === 'interactive' && typeof event.text === 'string') {
77
+ history.addUserMessage(event.text);
78
+ broadcastFn({ type: 'user_message', text: event.text });
79
+ }
80
+ });
81
+ }
@@ -0,0 +1,22 @@
1
+ export interface ToolSummary {
2
+ toolName: string;
3
+ args: unknown;
4
+ result: unknown;
5
+ isError: boolean;
6
+ }
7
+ export interface Turn {
8
+ role: 'user' | 'assistant';
9
+ text: string;
10
+ tools: ToolSummary[];
11
+ error?: boolean;
12
+ }
13
+ export declare class HistoryBuffer {
14
+ private entries;
15
+ private readonly limit;
16
+ constructor(limit?: number);
17
+ addUserMessage(text: string): void;
18
+ addAssistantTurn(text: string, tools: ToolSummary[]): void;
19
+ addError(text: string): void;
20
+ getEntries(): Turn[];
21
+ private _push;
22
+ }
@@ -0,0 +1,25 @@
1
+ export class HistoryBuffer {
2
+ entries = [];
3
+ limit;
4
+ constructor(limit = 20) {
5
+ this.limit = limit;
6
+ }
7
+ addUserMessage(text) {
8
+ this._push({ role: 'user', text, tools: [] });
9
+ }
10
+ addAssistantTurn(text, tools) {
11
+ this._push({ role: 'assistant', text, tools });
12
+ }
13
+ addError(text) {
14
+ this._push({ role: 'assistant', text, tools: [], error: true });
15
+ }
16
+ getEntries() {
17
+ return [...this.entries];
18
+ }
19
+ _push(entry) {
20
+ this.entries.push(entry);
21
+ if (this.entries.length > this.limit) {
22
+ this.entries.shift();
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,42 @@
1
+ export interface PromptMessage {
2
+ type: 'prompt';
3
+ id: string;
4
+ question: string;
5
+ recommended?: string;
6
+ allowSkip: boolean;
7
+ }
8
+ export interface PromptResolvedMessage {
9
+ type: 'prompt_resolved';
10
+ id: string;
11
+ }
12
+ export interface WidgetMessage {
13
+ type: 'widget';
14
+ key: string;
15
+ lines: string[] | null;
16
+ }
17
+ export interface NotifyMessage {
18
+ type: 'notify';
19
+ message: string;
20
+ level: 'info' | 'warning' | 'error';
21
+ }
22
+ export interface ViewerMessage {
23
+ type: 'viewer';
24
+ title: string;
25
+ text: string;
26
+ }
27
+ /** Server → browser messages added by the integration. The existing
28
+ * history / text_delta / tool_* / agent_* / client_count / user_message messages are
29
+ * emitted ad hoc by events.ts / server.ts and are not enumerated here. */
30
+ export type ServerMessage = PromptMessage | PromptResolvedMessage | WidgetMessage | NotifyMessage | ViewerMessage;
31
+ /** Browser → server messages. */
32
+ export interface ClientChatMessage {
33
+ type: 'message';
34
+ text: string;
35
+ }
36
+ export interface ClientPromptAnswer {
37
+ type: 'prompt_answer';
38
+ id: string;
39
+ value: string | undefined;
40
+ }
41
+ export type ClientMessage = ClientChatMessage | ClientPromptAnswer;
42
+ export declare function isClientMessage(x: unknown): x is ClientMessage;
@@ -0,0 +1,13 @@
1
+ // Wire format shared by the WS server and the inline browser app.
2
+ // Keep these shapes in sync with the hand-written switch in ui.ts.
3
+ export function isClientMessage(x) {
4
+ if (typeof x !== 'object' || x === null)
5
+ return false;
6
+ const m = x;
7
+ if (m.type === 'message')
8
+ return typeof m.text === 'string';
9
+ if (m.type === 'prompt_answer') {
10
+ return typeof m.id === 'string' && (m.value === undefined || typeof m.value === 'string');
11
+ }
12
+ return false;
13
+ }
@@ -0,0 +1 @@
1
+ export declare function qrLines(url: string): Promise<string[]>;
@@ -0,0 +1,5 @@
1
+ import qrcode from 'qrcode';
2
+ export async function qrLines(url) {
3
+ const raw = await qrcode.toString(url, { type: 'terminal', small: true });
4
+ return raw.split('\n').filter(l => l.length > 0);
5
+ }
@@ -0,0 +1,2 @@
1
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
+ export declare function registerRemote(pi: ExtensionAPI): void;