@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.
Files changed (44) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +237 -2
  3. package/bin/pocket.js +31 -0
  4. package/cordis.patch.yml +6 -0
  5. package/index.js +360 -0
  6. package/lib/client.js +2453 -0
  7. package/package.json +91 -4
  8. package/src/advertise-host.js +69 -0
  9. package/src/approval-hook.js +50 -0
  10. package/src/client/ConfirmDialog.tsx +80 -0
  11. package/src/client/HostUiLauncher.tsx +101 -0
  12. package/src/client/ManagePanel.tsx +218 -0
  13. package/src/client/PhoneIcon.tsx +48 -0
  14. package/src/client/PocketSettings.tsx +13 -0
  15. package/src/client/api.ts +97 -0
  16. package/src/client/completion-sync.js +289 -0
  17. package/src/client/host-ui-styles.ts +62 -0
  18. package/src/client/index.ts +47 -0
  19. package/src/client/locales.ts +75 -0
  20. package/src/client/nav-icon.ts +31 -0
  21. package/src/client/qr.ts +15 -0
  22. package/src/client/styles.ts +76 -0
  23. package/src/completion-route.js +87 -0
  24. package/src/completion-tracker.js +95 -0
  25. package/src/devices.js +60 -0
  26. package/src/extra-ops.js +604 -0
  27. package/src/host-ui.js +466 -0
  28. package/src/instance-id.js +33 -0
  29. package/src/iroh.js +260 -0
  30. package/src/manage.js +122 -0
  31. package/src/mdns-advertise.js +75 -0
  32. package/src/office-preview.js +129 -0
  33. package/src/pairing-uri.js +42 -0
  34. package/src/pairing.js +34 -0
  35. package/src/parity-ops.js +250 -0
  36. package/src/server.js +761 -0
  37. package/src/settings-ops.js +387 -0
  38. package/src/task-events.js +77 -0
  39. package/src/task-fallback.js +177 -0
  40. package/src/token.js +18 -0
  41. package/src/transport-pref.js +47 -0
  42. package/src/trust.js +25 -0
  43. package/src/user-question-hook.js +93 -0
  44. package/src/web-route.js +116 -0
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Remaining desktop-parity ops: search, workspace archive/pin, message feedback,
3
+ * @-references, attachments, file upload, user-questions answer.
4
+ */
5
+ const str = (v) => typeof v === 'string' && v.length > 0
6
+ const bad = (m) => Object.assign(new Error(m), { code: 'bad-request' })
7
+
8
+ function agentOf(agents, sessionId) {
9
+ if (!str(sessionId)) throw bad('sessionId required')
10
+ const agent = agents.get(sessionId)
11
+ if (!agent) throw Object.assign(new Error('session has no live agent'), { code: 'session/not-found' })
12
+ return agent
13
+ }
14
+
15
+ /** Reference discovery must resume persisted sessions, not only inspect the live Agent map. */
16
+ async function resolveReferenceAgent(sessionController, sessionId, signal) {
17
+ if (!str(sessionId)) throw bad('sessionId required')
18
+ signal?.throwIfAborted()
19
+ const result = await sessionController.resolveAgent(sessionId)
20
+ if ('error' in result) throw result.error
21
+ signal?.throwIfAborted()
22
+ return result.agent
23
+ }
24
+
25
+ /** `sessions.search` over sessionController.search. */
26
+ export function searchOps(sc) {
27
+ return {
28
+ 'sessions.search': async (a, signal) => {
29
+ if (!str(a.query)) throw bad('query required')
30
+ return sc.search({ query: a.query }, signal)
31
+ },
32
+ }
33
+ }
34
+
35
+ /** Workspace mutations use the official controller; state comes from its baseline. */
36
+ export function workspaceOps(workspaceController) {
37
+ const c = workspaceController
38
+ const ops = {
39
+ 'workspace.state': async (_a, signal) => {
40
+ const ac = new AbortController()
41
+ const abort = () => ac.abort()
42
+ signal?.addEventListener('abort', abort, { once: true })
43
+ if (signal?.aborted) ac.abort()
44
+ try {
45
+ for await (const frame of c.follow(ac.signal)) {
46
+ if (frame.type === 'baseline') return frame.value
47
+ }
48
+ throw Object.assign(new Error('workspace baseline unavailable'), { code: 'unavailable' })
49
+ } finally {
50
+ ac.abort()
51
+ signal?.removeEventListener('abort', abort)
52
+ }
53
+ },
54
+ }
55
+ for (const name of ['create', 'rename', 'delete', 'insertBefore', 'insertSessionBefore',
56
+ 'archiveSession', 'unarchiveSession', 'pinSession', 'unpinSession']) {
57
+ if (typeof c?.[name] === 'function') ops[`workspace.${name}`] = (a) => c[name](a)
58
+ }
59
+ return ops
60
+ }
61
+
62
+ /** Only advertise browse when the composed Host picker actually serves it. */
63
+ export function directoryOps(controller, directoryPicker) {
64
+ if (directoryPicker.capability().kind !== 'browse') return {}
65
+ const ops = {
66
+ 'workspace.directoryList': (a, signal) => controller.list(str(a.path) ? a.path : undefined, signal),
67
+ }
68
+ if (typeof controller.createDirectory === 'function') {
69
+ ops['workspace.directoryCreate'] = async (a) => {
70
+ if (!str(a?.path) || !a.path.trim()) throw bad('path required')
71
+ if (!str(a?.name) || !a.name.trim()) throw bad('name required')
72
+ return controller.createDirectory(a.path, a.name)
73
+ }
74
+ }
75
+ return ops
76
+ }
77
+
78
+ /** Goal refs are the official id/revision CAS fence, never inferred from an objective. */
79
+ export function goalOps(goals, agents) {
80
+ const ops = { 'goals.get': (a) => ({ goal: goals.get(agentOf(agents, a.sessionId)) ?? null }) }
81
+ for (const name of ['edit', 'pause', 'resume', 'clear']) {
82
+ ops[`goals.${name}`] = (a) => {
83
+ if (!str(a.ref?.id) || !Number.isSafeInteger(a.ref?.revision) || a.ref.revision < 1) throw bad('goal id/revision required')
84
+ const value = goals[name](agentOf(agents, a.sessionId), a.ref, a.request)
85
+ return name === 'clear' ? { cleared: value } : { goal: value }
86
+ }
87
+ }
88
+ return ops
89
+ }
90
+
91
+ /**
92
+ * Message like/dislike — Host `messageFeedback.list|put|delete`.
93
+ * Pocket names: feedback.get / feedback.set (set deletes when rating is null).
94
+ * The injected Host service returns the business Result directly, without a
95
+ * Typert RemoteResult carrier. Preserve the complete failure for Pocket clients.
96
+ */
97
+ export function feedbackOps(messageFeedback) {
98
+ const unwrap = (result) => {
99
+ if (!result.ok) {
100
+ throw Object.assign(new Error(result.error.code), { code: result.error.code, details: result.error })
101
+ }
102
+ return result.value
103
+ }
104
+ return {
105
+ 'feedback.get': async (a) => {
106
+ if (!str(a.sessionId)) throw bad('sessionId required')
107
+ return unwrap(await messageFeedback.list({ sessionId: a.sessionId }))
108
+ },
109
+ 'feedback.set': async (a) => {
110
+ if (!str(a.sessionId) || !str(a.messageId)) throw bad('sessionId and messageId required')
111
+ if (a.rating == null) {
112
+ return unwrap(await messageFeedback.delete({
113
+ sessionId: a.sessionId,
114
+ messageId: a.messageId,
115
+ ifVersion: a.ifVersion ?? null,
116
+ }))
117
+ }
118
+ if (a.rating !== 'positive' && a.rating !== 'negative') {
119
+ throw bad('rating must be positive, negative, or null')
120
+ }
121
+ return unwrap(await messageFeedback.put({
122
+ sessionId: a.sessionId,
123
+ messageId: a.messageId,
124
+ rating: a.rating,
125
+ ...(a.note !== undefined ? { note: a.note } : {}),
126
+ ...(a.category !== undefined ? { category: a.category } : {}),
127
+ ifVersion: a.ifVersion ?? null,
128
+ }))
129
+ },
130
+ }
131
+ }
132
+
133
+ /** `@` file mention candidates (`ctx.fileReferences`). */
134
+ export function fileReferenceOps(fileReferences, sessionController) {
135
+ return {
136
+ 'references.files': async (a, signal) => {
137
+ const agent = await resolveReferenceAgent(sessionController, a.sessionId, signal)
138
+ if (!fileReferences?.list) {
139
+ throw Object.assign(new Error('fileReferences unavailable'), { code: 'unavailable' })
140
+ }
141
+ const query = typeof a.query === 'string' ? a.query : ''
142
+ const items = await fileReferences.list(agent, query, signal)
143
+ return { items: items ?? [] }
144
+ },
145
+ }
146
+ }
147
+
148
+ /** `@` session / subagent mention candidates (`ctx.sessionReferenceResolver`). */
149
+ export function sessionReferenceOps(sessionReferenceResolver, sessionController, sessionQuery) {
150
+ return {
151
+ 'references.sessions': async (a, signal) => {
152
+ const agent = await resolveReferenceAgent(sessionController, a.sessionId, signal)
153
+ if (!sessionReferenceResolver?.remoteExportCandidates) {
154
+ throw Object.assign(new Error('sessionReferenceResolver unavailable'), { code: 'unavailable' })
155
+ }
156
+ const query = typeof a.query === 'string' ? a.query : ''
157
+ // `candidates` is the Remote name, not the Host service method.
158
+ const candidates = await sessionReferenceResolver.remoteExportCandidates(agent, query, signal)
159
+ if (candidates.length === 0) return { items: [] }
160
+ const records = await sessionQuery.listSessions(signal)
161
+ const children = new Set(records
162
+ .filter(({ header }) => header.origin === 'subagent' && header.parentSession === agent.id)
163
+ .map(({ header }) => header.id))
164
+ return {
165
+ items: candidates.map(candidate => ({
166
+ ...candidate,
167
+ title: candidate.displayTitle ?? candidate.label,
168
+ kind: children.has(candidate.sessionId) ? 'subagent' : 'session',
169
+ })),
170
+ }
171
+ },
172
+ }
173
+ }
174
+
175
+ /** Load image bytes for a session attachment (transcript bubbles). */
176
+ export function attachmentOps(sc) {
177
+ return {
178
+ 'session.attachment': async (a, signal) => {
179
+ if (!str(a.sessionId) || !str(a.attachmentId)) throw bad('sessionId and attachmentId required')
180
+ return sc.attachment({ sessionId: a.sessionId, attachmentId: a.attachmentId }, signal)
181
+ },
182
+ }
183
+ }
184
+
185
+ /** Serve Host-retained turn summaries and comparisons; only publish supported methods. */
186
+ export function workspaceChangesOps(workspaceChanges) {
187
+ const ops = {}
188
+ if (typeof workspaceChanges?.summary === 'function') {
189
+ ops['workspaceChanges.summary'] = (a) => {
190
+ if (!str(a.sessionId) || !Number.isSafeInteger(a.seq)) throw bad('sessionId and seq required')
191
+ return { summary: workspaceChanges.summary(a.sessionId, a.seq) ?? null }
192
+ }
193
+ }
194
+ if (typeof workspaceChanges?.diff === 'function') {
195
+ ops['workspaceChanges.diff'] = async (a, signal) => {
196
+ if (!str(a.sessionId) || !Number.isSafeInteger(a.seq)) throw bad('sessionId and seq required')
197
+ if (!Number.isSafeInteger(a.index) || a.index < 0) throw bad('non-negative safe index required')
198
+ return { diff: await workspaceChanges.diff(a.sessionId, a.seq, a.index, signal) ?? null }
199
+ }
200
+ }
201
+ return ops
202
+ }
203
+
204
+ /**
205
+ * Stage a non-image file for the next prompt via Host `fileUploads.upload`.
206
+ * Returns `{receiptId, file}` for `prompt` content `{type:'file', receiptId}`.
207
+ */
208
+ export function uploadOps(fileUploads, agents) {
209
+ return {
210
+ 'attachments.upload': async (a, signal) => {
211
+ if (!str(a.sessionId) || !str(a.data)) throw bad('sessionId and data required')
212
+ const agent = agentOf(agents, a.sessionId)
213
+ if (!fileUploads?.upload) {
214
+ throw Object.assign(new Error('fileUploads unavailable'), { code: 'unavailable' })
215
+ }
216
+ return fileUploads.upload(agent, {
217
+ data: a.data,
218
+ ...(str(a.name) ? { name: a.name } : {}),
219
+ }, signal)
220
+ },
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Answer an open / continued user-question.
226
+ * - If a phone waterfall offer is pending for `callId`, settle it (open ask).
227
+ * - Else forward to Host `userQuestions.answer` (continued projection).
228
+ */
229
+ export function userQuestionOps(userQuestions, sessionController, { settleOffer } = {}) {
230
+ return {
231
+ 'userQuestions.answer': async (a, signal, client) => {
232
+ if (!str(a.sessionId) || !str(a.callId)) throw bad('sessionId and callId required')
233
+ const items = a.answers?.answers ?? a.answers
234
+ if (!Array.isArray(items) || !items.every((item) => item && str(item.id) &&
235
+ Array.isArray(item.selected) && item.selected.every((label) => typeof label === 'string') &&
236
+ (item.custom === undefined || typeof item.custom === 'string'))) throw bad('answers required')
237
+ const answer = { answers: items }
238
+ signal?.throwIfAborted()
239
+ if (settleOffer?.(a.sessionId, a.callId, answer, client?.device?.id)) {
240
+ return { accepted: true, via: 'waterfall' }
241
+ }
242
+ const agent = await resolveReferenceAgent(sessionController, a.sessionId, signal)
243
+ if (!userQuestions?.answer) {
244
+ throw Object.assign(new Error('userQuestions unavailable'), { code: 'unavailable' })
245
+ }
246
+ const ok = await userQuestions.answer(agent, a.callId, answer)
247
+ return { accepted: Boolean(ok), via: 'host' }
248
+ },
249
+ }
250
+ }