@huakirax/dsh-plugin-pocket 0.0.0-stage → 0.1.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/LICENSE +9 -0
- package/README.md +237 -2
- package/bin/pocket.js +31 -0
- package/cordis.patch.yml +6 -0
- package/index.js +360 -0
- package/lib/client.js +2453 -0
- package/package.json +91 -4
- package/src/advertise-host.js +69 -0
- package/src/approval-hook.js +50 -0
- package/src/client/ConfirmDialog.tsx +80 -0
- package/src/client/HostUiLauncher.tsx +101 -0
- package/src/client/ManagePanel.tsx +218 -0
- package/src/client/PhoneIcon.tsx +48 -0
- package/src/client/PocketSettings.tsx +13 -0
- package/src/client/api.ts +97 -0
- package/src/client/completion-sync.js +289 -0
- package/src/client/host-ui-styles.ts +62 -0
- package/src/client/index.ts +47 -0
- package/src/client/locales.ts +75 -0
- package/src/client/nav-icon.ts +31 -0
- package/src/client/qr.ts +15 -0
- package/src/client/styles.ts +76 -0
- package/src/completion-route.js +87 -0
- package/src/completion-tracker.js +95 -0
- package/src/devices.js +60 -0
- package/src/extra-ops.js +604 -0
- package/src/host-ui.js +466 -0
- package/src/instance-id.js +33 -0
- package/src/iroh.js +260 -0
- package/src/manage.js +122 -0
- package/src/mdns-advertise.js +75 -0
- package/src/office-preview.js +129 -0
- package/src/pairing-uri.js +42 -0
- package/src/pairing.js +34 -0
- package/src/parity-ops.js +250 -0
- package/src/server.js +761 -0
- package/src/settings-ops.js +387 -0
- package/src/task-events.js +77 -0
- package/src/task-fallback.js +177 -0
- package/src/token.js +18 -0
- package/src/transport-pref.js +47 -0
- package/src/trust.js +25 -0
- package/src/user-question-hook.js +93 -0
- package/src/web-route.js +116 -0
package/index.js
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { createApprovalHook } from './src/approval-hook.js'
|
|
6
|
+
import { createUserQuestionHook } from './src/user-question-hook.js'
|
|
7
|
+
import { createDeviceStore } from './src/devices.js'
|
|
8
|
+
import { createPairing } from './src/pairing.js'
|
|
9
|
+
import { createPocketServer } from './src/server.js'
|
|
10
|
+
import { TaskEventHub, watchTaskCompletions } from './src/task-events.js'
|
|
11
|
+
import { TaskFallbackTracker } from './src/task-fallback.js'
|
|
12
|
+
import { loadOrCreateToken } from './src/token.js'
|
|
13
|
+
import { zipSync, strToU8 } from 'fflate'
|
|
14
|
+
import { permissionOps, presetOps, commandOps, skillOps, filesOps, jobsOps, exportOps, terminalOps } from './src/extra-ops.js'
|
|
15
|
+
import {
|
|
16
|
+
searchOps, workspaceOps, feedbackOps, fileReferenceOps, sessionReferenceOps,
|
|
17
|
+
attachmentOps, workspaceChangesOps, uploadOps, userQuestionOps, directoryOps, goalOps,
|
|
18
|
+
} from './src/parity-ops.js'
|
|
19
|
+
import {
|
|
20
|
+
modelsSettingsOps, pluginsOps, pluginSettingsOps, pluginInventoryOps, scheduleOps, credentialsOps,
|
|
21
|
+
} from './src/settings-ops.js'
|
|
22
|
+
import { registerPairRoute } from './src/web-route.js'
|
|
23
|
+
import { registerCompletionRoutes } from './src/completion-route.js'
|
|
24
|
+
import { createManageApi, pairingString } from './src/manage.js'
|
|
25
|
+
import { startIrohSidecar } from './src/iroh.js'
|
|
26
|
+
import { loadTransport, saveTransport, normalizeTransport } from './src/transport-pref.js'
|
|
27
|
+
import { listAdvertiseNetworks, pickBindHost, selectNetwork } from './src/advertise-host.js'
|
|
28
|
+
import { loadOrCreateInstanceId } from './src/instance-id.js'
|
|
29
|
+
import { startMdnsAdvertise } from './src/mdns-advertise.js'
|
|
30
|
+
import { createOfficePreviewOps } from './src/office-preview.js'
|
|
31
|
+
|
|
32
|
+
export const name = 'dsh-plugin-pocket'
|
|
33
|
+
// webServer is optional: Cordis `inject` entries are all required, so the
|
|
34
|
+
// loopback pairing route is mounted through a nested ctx.inject(['webServer'])
|
|
35
|
+
// that only runs when (and while) a webServer service exists.
|
|
36
|
+
export const inject = ['sessionController']
|
|
37
|
+
|
|
38
|
+
const SESSION_EVENTS = ['added', 'removed', 'status', 'activity', 'error']
|
|
39
|
+
const PKG = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'package.json'), 'utf8'))
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
43
|
+
* @param {{host?: string, port?: number, token?: string, dataDir?: string, pair?: boolean, iroh?: boolean, hostBinary?: string, transport?: string, pluginUiPaths?: string[]}} [config]
|
|
44
|
+
*/
|
|
45
|
+
export function apply(ctx, config = {}) {
|
|
46
|
+
const log = (...a) => console.log('[pocket]', ...a)
|
|
47
|
+
const dataDir = config.dataDir ?? join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'pocket')
|
|
48
|
+
const { token, created, path } = loadOrCreateToken(dataDir, config.token)
|
|
49
|
+
const devices = createDeviceStore({ dataDir, ownerToken: token })
|
|
50
|
+
const pairing = createPairing()
|
|
51
|
+
const listeners = new Set()
|
|
52
|
+
|
|
53
|
+
// Phone path is host:port WS only (Tailscale or LAN). Config may lock transport=tailscale.
|
|
54
|
+
let remoteTransport = loadTransport(dataDir, config.transport)
|
|
55
|
+
const configLocksTransport = Boolean(normalizeTransport(config.transport))
|
|
56
|
+
|
|
57
|
+
const port = config.port ?? 7690
|
|
58
|
+
const bindHost = pickBindHost(config.host)
|
|
59
|
+
const instanceId = loadOrCreateInstanceId(dataDir)
|
|
60
|
+
/** @type {{ stop: () => Promise<void>, deviceName: string } | null} */
|
|
61
|
+
let mdns = null
|
|
62
|
+
const getNetworks = () => {
|
|
63
|
+
const address = server.address()
|
|
64
|
+
return address && typeof address === 'object'
|
|
65
|
+
? listAdvertiseNetworks(config.host, address.address)
|
|
66
|
+
: []
|
|
67
|
+
}
|
|
68
|
+
const taskHub = new TaskEventHub()
|
|
69
|
+
/** @type {TaskFallbackTracker | null} */
|
|
70
|
+
let taskFallback = null
|
|
71
|
+
const server = createPocketServer({
|
|
72
|
+
sessionController: ctx.sessionController, token, host: bindHost, port, devices, pairing,
|
|
73
|
+
getSubagents: () => ctx.get('subagents'),
|
|
74
|
+
info: {
|
|
75
|
+
plugin: PKG.name,
|
|
76
|
+
version: PKG.version,
|
|
77
|
+
instanceId,
|
|
78
|
+
// Phone VersionCompatibility requires this floor; bump together with app minSdk policy.
|
|
79
|
+
minimumAppVersion: '0.1.0',
|
|
80
|
+
},
|
|
81
|
+
onSessionEvent: (fn) => { listeners.add(fn); return () => listeners.delete(fn) },
|
|
82
|
+
onSessionLifecycle: (kind, args) => {
|
|
83
|
+
if (!taskFallback) return
|
|
84
|
+
if (kind === 'status') taskFallback.onStatus(args[0], Boolean(args[1]))
|
|
85
|
+
else if (kind === 'activity') taskFallback.markActivity(args[0], typeof args[1] === 'number' ? args[1] : undefined)
|
|
86
|
+
else if (kind === 'removed') taskFallback.forget(args[0])
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
taskHub.add({ broadcastTaskEvent: (event) => server.broadcastTaskEvent(event) })
|
|
90
|
+
taskFallback = new TaskFallbackTracker({
|
|
91
|
+
onTaskCompleted: (event) => taskHub.broadcast(event),
|
|
92
|
+
isRootSession: (sessionId) => server.isRootSession(sessionId),
|
|
93
|
+
hasPendingQuestion: (sessionId) => server.hasPendingUserQuestionForSession(sessionId),
|
|
94
|
+
})
|
|
95
|
+
// Adapted from saya-ch/dsh-mobile (Apache-2.0): exact root turn/end completions.
|
|
96
|
+
const disposeTaskEvents = watchTaskCompletions(ctx, {
|
|
97
|
+
onTaskCompleted: (event) => {
|
|
98
|
+
taskFallback?.markAnnounced(event.sessionId)
|
|
99
|
+
taskHub.broadcast(event)
|
|
100
|
+
},
|
|
101
|
+
log: (event, fields) => log(`task ${event}`, fields),
|
|
102
|
+
})
|
|
103
|
+
for (const kind of SESSION_EVENTS) {
|
|
104
|
+
ctx.on(`api-session/${kind}`, (...args) => { for (const fn of listeners) fn(kind, args) })
|
|
105
|
+
}
|
|
106
|
+
ctx.inject(['sessionProjections'], (c) => {
|
|
107
|
+
c.effect(() => c.sessionProjections.onChanged((session, key, value, seq) => {
|
|
108
|
+
server.broadcastProjection(session.id, key, value, seq)
|
|
109
|
+
}))
|
|
110
|
+
})
|
|
111
|
+
// prepend: run outside the web UI forwarder so we can race it.
|
|
112
|
+
ctx.on('approval/request', createApprovalHook(server, { log }), { prepend: true })
|
|
113
|
+
|
|
114
|
+
/** Optional legacy desktop iroh sidecar — off unless config.iroh === true. Not on the phone path. */
|
|
115
|
+
let iroh = null
|
|
116
|
+
|
|
117
|
+
const getTransport = () => remoteTransport
|
|
118
|
+
const setTransportMode = (mode) => {
|
|
119
|
+
if (configLocksTransport) {
|
|
120
|
+
throw new Error(`transport is locked by cordis config (${config.transport})`)
|
|
121
|
+
}
|
|
122
|
+
remoteTransport = saveTransport(dataDir, mode)
|
|
123
|
+
log(`transport set to ${remoteTransport}`)
|
|
124
|
+
return remoteTransport
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const openPairing = ({ type, host, port }) => {
|
|
128
|
+
const mode = remoteTransport
|
|
129
|
+
const w = pairing.open()
|
|
130
|
+
const line = pairingString({ code: w.code, host, port, transport: mode })
|
|
131
|
+
log(`pairing (${type}, ${Math.round(w.expiresInSec / 60)} min): ${line}`)
|
|
132
|
+
}
|
|
133
|
+
const manage = createManageApi({
|
|
134
|
+
getNetworks, port, devices, pairing, server, openPairing,
|
|
135
|
+
getTransport,
|
|
136
|
+
setTransport: setTransportMode,
|
|
137
|
+
instanceId,
|
|
138
|
+
getMdns: () => mdns !== null,
|
|
139
|
+
})
|
|
140
|
+
ctx.on('permission-presets/catalog-changed', () => server.broadcastPermissionCatalogChanged())
|
|
141
|
+
ctx.inject(['permissionPresets', 'agents'], (c) => {
|
|
142
|
+
c.effect(() => {
|
|
143
|
+
const unregister = server.registerOps(permissionOps(c.permissionPresets, c.agents))
|
|
144
|
+
server.broadcastPermissionCatalogChanged()
|
|
145
|
+
return () => {
|
|
146
|
+
unregister()
|
|
147
|
+
server.broadcastPermissionCatalogChanged()
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
ctx.inject(['agentPresets'], (c) => {
|
|
152
|
+
c.effect(() => server.registerOps(presetOps(c.agentPresets)))
|
|
153
|
+
c.inject(['settingsController'], (settingsCtx) => {
|
|
154
|
+
settingsCtx.effect(() => server.registerOps({
|
|
155
|
+
'presets.settings': presetOps(settingsCtx.agentPresets, settingsCtx.settingsController)['presets.settings'],
|
|
156
|
+
}))
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
ctx.inject(['commands', 'agents'], (c) => {
|
|
160
|
+
c.effect(() => server.registerOps(commandOps(c.commands, c.agents, c.get?.('agentPresets'))))
|
|
161
|
+
c.inject(['agentPresets'], (p) => {
|
|
162
|
+
p.effect(() => server.registerOps(commandOps(p.commands, p.agents, p.agentPresets)))
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
ctx.inject(['sessionSkillCatalog'], (c) => {
|
|
166
|
+
c.effect(() => server.registerOps(skillOps(c.sessionSkillCatalog, {
|
|
167
|
+
skills: c.get?.('skills'),
|
|
168
|
+
workingDirectory: c.get?.('workingDirectory'),
|
|
169
|
+
agentPresets: c.get?.('agentPresets'),
|
|
170
|
+
})))
|
|
171
|
+
c.inject(['skills', 'workingDirectory', 'agentPresets'], (full) => {
|
|
172
|
+
full.effect(() => server.registerOps(skillOps(full.sessionSkillCatalog, {
|
|
173
|
+
skills: full.skills,
|
|
174
|
+
workingDirectory: full.workingDirectory,
|
|
175
|
+
agentPresets: full.agentPresets,
|
|
176
|
+
})))
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
ctx.inject(['workspaceFiles', 'sessions'], (c) => {
|
|
180
|
+
c.effect(() => server.registerOps(filesOps(c.workspaceFiles, c.sessions, c.get?.('sandboxPolicy'))))
|
|
181
|
+
})
|
|
182
|
+
ctx.inject(['officeToPdf', 'workspaceFiles', 'sessions', 'sandboxPolicy'], (c) => {
|
|
183
|
+
c.effect(() => {
|
|
184
|
+
const preview = createOfficePreviewOps(c)
|
|
185
|
+
const unregister = server.registerOps(preview.ops)
|
|
186
|
+
return () => { preview.dispose(); unregister() }
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
ctx.inject(['jobController'], (c) => { c.effect(() => server.registerOps(jobsOps(c.jobController))) })
|
|
190
|
+
ctx.inject(['terminalController', 'agents'], (c) => {
|
|
191
|
+
c.effect(() => server.registerOps(terminalOps(c.terminalController, c.agents)))
|
|
192
|
+
})
|
|
193
|
+
server.registerOps(exportOps(ctx.sessionController, { zipSync, strToU8 }))
|
|
194
|
+
server.registerOps(searchOps(ctx.sessionController))
|
|
195
|
+
server.registerOps(attachmentOps(ctx.sessionController))
|
|
196
|
+
ctx.inject(['workspaceChanges'], (c) => {
|
|
197
|
+
c.effect(() => server.registerOps(workspaceChangesOps(c.workspaceChanges)))
|
|
198
|
+
})
|
|
199
|
+
ctx.inject(['workspaceController'], (c) => {
|
|
200
|
+
c.effect(() => {
|
|
201
|
+
const unregister = server.registerOps(workspaceOps(c.workspaceController))
|
|
202
|
+
const ac = new AbortController()
|
|
203
|
+
void (async () => {
|
|
204
|
+
try {
|
|
205
|
+
for await (const frame of c.workspaceController.follow(ac.signal)) {
|
|
206
|
+
if (ac.signal.aborted) break
|
|
207
|
+
server.broadcastEvent('workspace.changed', { frame })
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (!ac.signal.aborted) log('workspace follow failed', String(error?.message ?? error))
|
|
211
|
+
}
|
|
212
|
+
})()
|
|
213
|
+
return () => { ac.abort(); unregister() }
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
ctx.inject(['directoryPickerController', 'directoryPicker'], (c) => {
|
|
217
|
+
c.effect(() => server.registerOps(directoryOps(c.directoryPickerController, c.directoryPicker)))
|
|
218
|
+
})
|
|
219
|
+
ctx.inject(['goals', 'agents'], (c) => {
|
|
220
|
+
c.effect(() => server.registerOps(goalOps(c.goals, c.agents)))
|
|
221
|
+
})
|
|
222
|
+
ctx.on('goal/activation-changed', (payload) => server.broadcastEvent('goal.activation', payload))
|
|
223
|
+
ctx.on('schedule/changed', () => server.broadcastEvent('schedule.changed'))
|
|
224
|
+
ctx.on('settings/document-updated', (ns, revision) => server.broadcastEvent('settings.changed', { ns, revision }))
|
|
225
|
+
ctx.on('credentials/reference-updated', () => server.broadcastEvent('credentials.changed'))
|
|
226
|
+
ctx.on('credentials/record-updated', () => server.broadcastEvent('credentials.changed'))
|
|
227
|
+
ctx.on('llm/adapters-updated', () => server.broadcastEvent('models.changed'))
|
|
228
|
+
ctx.on('plugin-manager/changed', () => server.broadcastEvent('plugins.changed'))
|
|
229
|
+
ctx.on('agent-preset/selected', (sessionId) => server.broadcastEvent('presets.changed', { sessionId }))
|
|
230
|
+
ctx.inject(['messageFeedback'], (c) => {
|
|
231
|
+
c.effect(() => server.registerOps(feedbackOps(c.messageFeedback)))
|
|
232
|
+
})
|
|
233
|
+
ctx.inject(['sessionFeedback'], (c) => {
|
|
234
|
+
c.effect(() => server.registerOps({
|
|
235
|
+
'sessionFeedback.record': async (a) => {
|
|
236
|
+
const result = await c.sessionFeedback.record(a)
|
|
237
|
+
if (!result.ok) throw Object.assign(new Error(result.error.code), { code: result.error.code, details: result.error })
|
|
238
|
+
return result.value
|
|
239
|
+
},
|
|
240
|
+
}))
|
|
241
|
+
})
|
|
242
|
+
ctx.inject(['fileReferences', 'sessionController'], (c) => {
|
|
243
|
+
c.effect(() => server.registerOps(fileReferenceOps(c.fileReferences, c.sessionController)))
|
|
244
|
+
})
|
|
245
|
+
ctx.inject(['sessionReferenceResolver', 'sessionController', 'sessionQuery'], (c) => {
|
|
246
|
+
c.effect(() => server.registerOps(sessionReferenceOps(c.sessionReferenceResolver, c.sessionController, c.sessionQuery)))
|
|
247
|
+
})
|
|
248
|
+
ctx.inject(['fileUploads', 'agents'], (c) => {
|
|
249
|
+
c.effect(() => server.registerOps(uploadOps(c.fileUploads, c.agents)))
|
|
250
|
+
})
|
|
251
|
+
ctx.inject(['userQuestions', 'sessionController'], (c) => {
|
|
252
|
+
c.on('user-questions/request', createUserQuestionHook(server, { userQuestions: c.userQuestions }), { prepend: true })
|
|
253
|
+
c.effect(() => server.registerOps(userQuestionOps(c.userQuestions, c.sessionController, {
|
|
254
|
+
settleOffer: (sessionId, callId, answer, deviceId) => server.settleUserQuestion(sessionId, callId, answer, deviceId),
|
|
255
|
+
})))
|
|
256
|
+
})
|
|
257
|
+
// Desktop settings surfaces (models providers / plugins / schedules).
|
|
258
|
+
// Each inject is optional; ops appear in hello only while the Host service is live.
|
|
259
|
+
ctx.inject(['llm', 'settingsController'], (c) => {
|
|
260
|
+
c.effect(() => {
|
|
261
|
+
const settings = c.get?.('settings') ?? null
|
|
262
|
+
const settingsController = c.settingsController
|
|
263
|
+
const credentials = c.get?.('credentials') ?? null
|
|
264
|
+
return server.registerOps(modelsSettingsOps({
|
|
265
|
+
llm: c.llm, settings, settingsController, credentials,
|
|
266
|
+
}))
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
ctx.inject(['pluginManager'], (c) => {
|
|
270
|
+
c.effect(() => server.registerOps(pluginsOps(c.pluginManager)))
|
|
271
|
+
})
|
|
272
|
+
ctx.inject(['pluginInventory'], (c) => {
|
|
273
|
+
c.effect(() => server.registerOps(pluginInventoryOps(c.pluginInventory)))
|
|
274
|
+
})
|
|
275
|
+
ctx.inject(['settingsController'], (c) => {
|
|
276
|
+
c.effect(() => server.registerOps(pluginSettingsOps(c.settingsController)))
|
|
277
|
+
})
|
|
278
|
+
ctx.inject(['schedule'], (c) => {
|
|
279
|
+
c.effect(() => server.registerOps(scheduleOps(c.schedule)))
|
|
280
|
+
})
|
|
281
|
+
ctx.inject(['credentials'], (c) => {
|
|
282
|
+
c.effect(() => server.registerOps(credentialsOps({ credentials: c.credentials })))
|
|
283
|
+
})
|
|
284
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
285
|
+
registerPairRoute(webCtx, manage)
|
|
286
|
+
registerCompletionRoutes(webCtx, server.completion)
|
|
287
|
+
})
|
|
288
|
+
ctx.inject(['webServer', 'connection'], (webCtx) => {
|
|
289
|
+
webCtx.effect(() => server.installHostUi({
|
|
290
|
+
webServer: webCtx.webServer,
|
|
291
|
+
connection: webCtx.connection,
|
|
292
|
+
pluginUiPaths: config.pluginUiPaths,
|
|
293
|
+
}))
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
ctx.effect(() => {
|
|
297
|
+
const start = async () => {
|
|
298
|
+
log(`remote transport: ${remoteTransport} (bind ${bindHost}:${port})`)
|
|
299
|
+
|
|
300
|
+
// Legacy opt-in only — phone APK no longer ships iroh; keep server code dormant by default.
|
|
301
|
+
if (config.iroh === true) {
|
|
302
|
+
iroh = await startIrohSidecar({
|
|
303
|
+
server,
|
|
304
|
+
dataDir: join(dataDir, 'iroh'),
|
|
305
|
+
hostBinary: config.hostBinary,
|
|
306
|
+
log,
|
|
307
|
+
})
|
|
308
|
+
if (iroh) {
|
|
309
|
+
try {
|
|
310
|
+
await Promise.race([
|
|
311
|
+
iroh.endpoint,
|
|
312
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error('iroh ready timeout')), 60_000)),
|
|
313
|
+
])
|
|
314
|
+
log('legacy iroh sidecar ready (not used for phone pairing)')
|
|
315
|
+
} catch (e) {
|
|
316
|
+
console.error('[pocket] iroh sidecar did not become ready:', e.message ?? e)
|
|
317
|
+
await iroh.close()
|
|
318
|
+
iroh = null
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const addr = await server.listen()
|
|
324
|
+
const selected = selectNetwork(getNetworks())
|
|
325
|
+
const listenAddress = server.address()
|
|
326
|
+
const listenPort = listenAddress && typeof listenAddress === 'object' ? listenAddress.port : port
|
|
327
|
+
log(`ws listening on ws://${addr} (pairing ${selected ? `${selected.type}: ${selected.host}:${listenPort}` : 'unavailable: no reachable network'})`)
|
|
328
|
+
log(`instance ${instanceId.slice(0, 8)}…`)
|
|
329
|
+
log(created ? `new owner token written to ${path}: ${token}` : `owner token: ${path}`)
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
if (selected) {
|
|
333
|
+
mdns = startMdnsAdvertise({
|
|
334
|
+
host: selected.host,
|
|
335
|
+
port: listenPort,
|
|
336
|
+
instanceId,
|
|
337
|
+
log,
|
|
338
|
+
onError: (err) => console.error('[pocket] mdns:', err.message ?? err),
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
} catch (e) {
|
|
342
|
+
console.error('[pocket] mdns advertise failed:', e.message ?? e)
|
|
343
|
+
mdns = null
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (config.pair || devices.list().length === 0) {
|
|
347
|
+
try { manage.openPairing() } catch (e) { console.error('[pocket]', e.message ?? e) }
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
start().catch((error) => console.error('[pocket] start failed:', error))
|
|
351
|
+
return () => {
|
|
352
|
+
void mdns?.stop()
|
|
353
|
+
mdns = null
|
|
354
|
+
disposeTaskEvents()
|
|
355
|
+
taskFallback?.dispose()
|
|
356
|
+
void iroh?.close()
|
|
357
|
+
void server.close()
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
}
|