dsh-reference-anything 0.2.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 (66) hide show
  1. package/LICENSE +22 -0
  2. package/NOTICE.md +54 -0
  3. package/README.md +228 -0
  4. package/cordis.patch.yml +63 -0
  5. package/images/logo.png +0 -0
  6. package/lib/client/index.d.ts +3 -0
  7. package/lib/client.js +18151 -0
  8. package/lib/contract.d.ts +300 -0
  9. package/lib/contract.js +102 -0
  10. package/lib/errors.d.ts +56 -0
  11. package/lib/errors.js +21 -0
  12. package/lib/host.d.ts +102 -0
  13. package/lib/host.js +55 -0
  14. package/lib/index.d.ts +83 -0
  15. package/lib/index.js +153 -0
  16. package/lib/opencli.d.ts +116 -0
  17. package/lib/opencli.js +356 -0
  18. package/lib/reference.d.ts +49 -0
  19. package/lib/reference.js +228 -0
  20. package/lib/render.d.ts +101 -0
  21. package/lib/render.js +250 -0
  22. package/lib/retain.d.ts +35 -0
  23. package/lib/retain.js +116 -0
  24. package/lib/search.d.ts +75 -0
  25. package/lib/search.js +126 -0
  26. package/lib/serialize.d.ts +22 -0
  27. package/lib/serialize.js +24 -0
  28. package/lib/sources/file.d.ts +100 -0
  29. package/lib/sources/file.js +236 -0
  30. package/lib/sources/web-chat/index.d.ts +114 -0
  31. package/lib/sources/web-chat/index.js +359 -0
  32. package/lib/store/spec.d.ts +312 -0
  33. package/lib/store/spec.js +94 -0
  34. package/lib/store/store.d.ts +189 -0
  35. package/lib/store/store.js +638 -0
  36. package/lib/sync/index.d.ts +91 -0
  37. package/lib/sync/index.js +425 -0
  38. package/lib/tool.d.ts +62 -0
  39. package/lib/tool.js +364 -0
  40. package/lib/typert.d.ts +2 -0
  41. package/lib/typert.js +34 -0
  42. package/lib/types.d.ts +215 -0
  43. package/lib/types.js +12 -0
  44. package/lib/uri-codec.d.ts +5 -0
  45. package/lib/uri-codec.js +39 -0
  46. package/lib/uri.d.ts +59 -0
  47. package/lib/uri.js +86 -0
  48. package/lib/web.d.ts +4 -0
  49. package/lib/web.js +11 -0
  50. package/lib/window.d.ts +23 -0
  51. package/lib/window.js +35 -0
  52. package/lib/wire.d.ts +117 -0
  53. package/lib/wire.js +49 -0
  54. package/lib/workspace.d.ts +18 -0
  55. package/lib/workspace.js +81 -0
  56. package/opencli-plugin/chatgpt.js +125 -0
  57. package/opencli-plugin/claude.js +134 -0
  58. package/opencli-plugin/common.js +307 -0
  59. package/opencli-plugin/deepseek.js +152 -0
  60. package/opencli-plugin/gemini.js +165 -0
  61. package/opencli-plugin/grok.js +123 -0
  62. package/opencli-plugin/kimi.js +123 -0
  63. package/opencli-plugin/opencli-plugin.json +6 -0
  64. package/opencli-plugin/package.json +8 -0
  65. package/opencli-plugin/since-guard.js +45 -0
  66. package/package.json +180 -0
@@ -0,0 +1,134 @@
1
+ import { domFallbackScript, registerProvider, sinceGuardSource } from './common.js'
2
+ // OpenCLI discovery marker: registerProvider below performs the cli() calls.
3
+
4
+ const shared = String.raw`
5
+ const organization = async () => {
6
+ const response = await fetch('/api/organizations', { credentials: 'include' })
7
+ if (response.status === 401 || response.status === 403) throw Object.assign(new Error('login required'), { code: 'AUTH' })
8
+ if (!response.ok) throw new Error('organizations HTTP ' + response.status)
9
+ const payload = await response.json()
10
+ const organizations = Array.isArray(payload) ? payload : payload.organizations || []
11
+ const item = organizations.find(value => value && (value.uuid || value.id))
12
+ if (!item) throw Object.assign(new Error('no Claude organization'), { code: 'AUTH' })
13
+ return String(item.uuid || item.id)
14
+ }
15
+ `
16
+
17
+ const historyScript = String.raw`async function (args) {
18
+ ${shared}
19
+ const stopEarly = (${sinceGuardSource})(args && args.since)
20
+ const rows = []
21
+ try {
22
+ const org = await organization()
23
+ let offset = 0
24
+ for (let page = 0; page < 500; page++) {
25
+ const response = await fetch('/api/organizations/' + encodeURIComponent(org) + '/chat_conversations?limit=100&offset=' + offset, { credentials: 'include' })
26
+ if (response.status === 401 || response.status === 403) return JSON.stringify({ ok: false, code: 'AUTH' })
27
+ if (response.status === 429) return JSON.stringify({ ok: false, code: 'RATE_LIMIT' })
28
+ if (!response.ok) throw new Error('history HTTP ' + response.status)
29
+ const payload = await response.json()
30
+ const items = Array.isArray(payload) ? payload : payload.items || payload.chat_conversations || []
31
+ const pageStart = rows.length
32
+ for (const item of items) {
33
+ const id = String(item.uuid || item.id || '')
34
+ if (!id) continue
35
+ rows.push({ provider: 'claude', accountScope: '', id,
36
+ title: String(item.name || item.title || 'Untitled Claude chat'),
37
+ url: location.origin + '/chat/' + encodeURIComponent(id),
38
+ createdAt: String(item.created_at || item.createdAt || ''),
39
+ updatedAt: String(item.updated_at || item.updatedAt || item.created_at || ''),
40
+ messageCount: Number(item.message_count || item.messageCount || 0),
41
+ cursor: String(offset + items.length), partial: false })
42
+ }
43
+ if (items.length < 100) break
44
+ if (stopEarly(rows.slice(pageStart))) break
45
+ offset += items.length
46
+ }
47
+ return JSON.stringify({ ok: true, rows })
48
+ } catch (error) {
49
+ if (error && error.code === 'AUTH') return JSON.stringify({ ok: false, code: 'AUTH' })
50
+ const links = Array.from(document.querySelectorAll('a[href*="/chat/"]'))
51
+ const seen = new Set()
52
+ for (const link of links) {
53
+ const match = link.href.match(/\/chat\/([^/?#]+)/)
54
+ if (!match || seen.has(match[1])) continue
55
+ seen.add(match[1])
56
+ rows.push({ provider: 'claude', accountScope: '', id: match[1],
57
+ title: (link.textContent || '').trim() || 'Untitled Claude chat', url: link.href,
58
+ createdAt: '', updatedAt: '', messageCount: 0, cursor: '', partial: true })
59
+ }
60
+ return JSON.stringify({ ok: rows.length > 0, rows, message: String(error && error.message || error) })
61
+ }
62
+ }`
63
+
64
+ const detailScript = String.raw`async function (args) {
65
+ ${shared}
66
+ const textOf = value => typeof value === 'string' ? value
67
+ : Array.isArray(value) ? value.map(textOf).filter(Boolean).join('\n')
68
+ : value && typeof value === 'object' ? textOf(value.text || value.content || value.value || value.markdown || '') : ''
69
+ try {
70
+ const org = await organization()
71
+ const url = '/api/organizations/' + encodeURIComponent(org) + '/chat_conversations/' + encodeURIComponent(args.id)
72
+ + '?tree=True&rendering_mode=messages&render_all_tools=true'
73
+ const response = await fetch(url, { credentials: 'include' })
74
+ if (response.status === 401 || response.status === 403) return JSON.stringify({ ok: false, code: 'AUTH' })
75
+ if (response.status === 429) return JSON.stringify({ ok: false, code: 'RATE_LIMIT' })
76
+ if (!response.ok) throw new Error('detail HTTP ' + response.status)
77
+ const payload = await response.json()
78
+ const messages = payload.chat_messages || payload.messages || (payload.conversation && payload.conversation.chat_messages) || []
79
+ const byId = new Map(messages.map(message => [String(message.uuid || message.id || ''), message]))
80
+ const childIds = new Set(messages.map(message => String(message.parent_message_uuid || message.parent_uuid || message.parent_id || '')).filter(Boolean))
81
+ const requestedLeaf = String(payload.current_leaf_message_uuid || payload.current_message_uuid || payload.current_node_uuid || '')
82
+ let leaf = requestedLeaf && byId.has(requestedLeaf) ? requestedLeaf : ''
83
+ if (!leaf) {
84
+ const leaves = messages.filter(message => !childIds.has(String(message.uuid || message.id || '')))
85
+ leaf = String((leaves[leaves.length - 1] || {}).uuid || (leaves[leaves.length - 1] || {}).id || '')
86
+ }
87
+ const active = new Set()
88
+ for (let id = leaf, guard = 0; id && guard++ < messages.length + 1;) {
89
+ active.add(id)
90
+ const message = byId.get(id)
91
+ id = message ? String(message.parent_message_uuid || message.parent_uuid || message.parent_id || '') : ''
92
+ }
93
+ const ordered = messages.slice().sort((a, b) => String(a.created_at || '').localeCompare(String(b.created_at || '')))
94
+ const rows = ordered.map((message, ordinal) => {
95
+ const id = String(message.uuid || message.id || ordinal)
96
+ const sender = String(message.sender || message.role || '').toLowerCase()
97
+ const content = message.content || message.text || message.content_blocks || ''
98
+ const attachments = (message.attachments || message.files || []).map((file, index) => {
99
+ const candidate = String(file.download_url || file.url || '')
100
+ let locator = ''
101
+ try { if (candidate) { const url = new URL(candidate, location.origin); if (url.origin === location.origin && url.pathname !== '/') locator = url.pathname + url.search } } catch {}
102
+ const name = String(file.name || file.file_name || 'attachment')
103
+ const mimeType = String(file.mime_type || file.mimeType || '')
104
+ return { attachmentId: String(file.id || file.uuid || index), name: String(file.name || file.file_name || 'attachment'),
105
+ kind: mimeType.startsWith('image/') || /\.(?:gif|jpe?g|png|webp)$/i.test(name) ? 'image' : 'file',
106
+ mimeType, size: Number(file.size || 0),
107
+ locator, status: locator ? 'available' : 'unavailable' }
108
+ })
109
+ return { conversationId: args.id, ordinal, messageId: id,
110
+ parentId: String(message.parent_message_uuid || message.parent_uuid || message.parent_id || ''),
111
+ branchId: String(message.branch_uuid || ''), activeBranch: active.size ? active.has(id) : true,
112
+ role: /assistant|claude|model/.test(sender) ? 'assistant' : 'user', text: textOf(content),
113
+ createdAt: String(message.created_at || message.createdAt || ''), attachmentsJson: JSON.stringify(attachments), partial: false }
114
+ }).filter(row => row.text || row.attachmentsJson !== '[]')
115
+ return JSON.stringify({ ok: rows.length > 0, rows })
116
+ } catch (error) {
117
+ if (error && error.code === 'AUTH') return JSON.stringify({ ok: false, code: 'AUTH' })
118
+ return JSON.stringify({ ok: false, message: String(error && error.message || error) })
119
+ }
120
+ }`
121
+
122
+ registerProvider({
123
+ site: 'dsh-claude', provider: 'Claude', domain: 'claude.ai', home: 'https://claude.ai/',
124
+ conversationUrl: id => `https://claude.ai/chat/${encodeURIComponent(id)}`,
125
+ whoamiScript: String.raw`async function () {
126
+ const response = await fetch('/api/organizations', { credentials: 'include' })
127
+ if (response.status === 401 || response.status === 403) return JSON.stringify({ ok: false, code: 'AUTH' })
128
+ if (!response.ok) return JSON.stringify({ ok: false, message: 'organizations HTTP ' + response.status })
129
+ const payload = await response.json(); const values = Array.isArray(payload) ? payload : payload.organizations || []
130
+ const item = values.find(value => value && (value.uuid || value.id)); const identity = String(item && (item.uuid || item.id) || '')
131
+ return JSON.stringify({ ok: Boolean(identity), identity, code: identity ? undefined : 'AUTH' })
132
+ }`,
133
+ historyScript, detailScript, fallbackScript: domFallbackScript,
134
+ })
@@ -0,0 +1,307 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises'
2
+ import { createHash } from 'node:crypto'
3
+ import { dirname, resolve } from 'node:path'
4
+ import { cli, Strategy } from '@jackwener/opencli/registry'
5
+ import { sinceGuardSource } from './since-guard.js'
6
+ import {
7
+ ArgumentError,
8
+ AuthRequiredError,
9
+ CommandExecutionError,
10
+ EmptyResultError,
11
+ } from '@jackwener/opencli/errors'
12
+
13
+ export { sinceGuardSource }
14
+
15
+ const HISTORY_COLUMNS = [
16
+ 'provider', 'accountScope', 'id', 'title', 'url', 'createdAt', 'updatedAt',
17
+ 'messageCount', 'cursor', 'partial',
18
+ ]
19
+
20
+ const SYNC_INDEX_COLUMNS = ['kind', 'identity', 'sinceApplied', ...HISTORY_COLUMNS]
21
+
22
+ const DETAIL_COLUMNS = [
23
+ 'conversationId', 'ordinal', 'messageId', 'parentId', 'branchId',
24
+ 'activeBranch', 'role', 'text', 'createdAt', 'attachmentsJson', 'partial',
25
+ ]
26
+
27
+ function parseEvaluate(value, operation) {
28
+ const raw = typeof value === 'string' ? value : JSON.stringify(value)
29
+ try {
30
+ return JSON.parse(raw)
31
+ } catch (error) {
32
+ throw new CommandExecutionError(`${operation}: provider page returned malformed JSON: ${String(error)}`)
33
+ }
34
+ }
35
+
36
+ async function evaluate(page, script, args, operation) {
37
+ const source = `(() => { const __args = ${JSON.stringify(args)}; return (${script})(__args); })()`
38
+ return parseEvaluate(await page.evaluate(source), operation)
39
+ }
40
+
41
+ function assertResult(result, domain, operation) {
42
+ if (result?.code === 'AUTH') throw new AuthRequiredError(domain, result.message || `${operation} requires login`)
43
+ if (result?.code === 'RATE_LIMIT') {
44
+ throw new CommandExecutionError(`${operation}: ${result.message || 'provider rate limit reached'}`)
45
+ }
46
+ if (result?.ok !== true || !Array.isArray(result.rows)) {
47
+ throw new CommandExecutionError(`${operation}: ${result?.message || 'provider adapter did not return rows'}`)
48
+ }
49
+ return result.rows
50
+ }
51
+
52
+ /**
53
+ * Release only the one-shot task tab. Browser Bridge deliberately keeps its
54
+ * empty adapter container alive so the next sync can reuse it; if the user
55
+ * closes that container, Browser Bridge recreates it on demand.
56
+ */
57
+ async function inTemporaryTab(page, task) {
58
+ try {
59
+ return await task()
60
+ } finally {
61
+ try {
62
+ await page.closeTab?.()
63
+ } catch {
64
+ // Preserve the provider result/error if the browser closed first.
65
+ }
66
+ }
67
+ }
68
+
69
+ export function registerProvider(config) {
70
+ const browserSession = SYNC_BROWSER_SESSION
71
+
72
+ cli({
73
+ site: config.site,
74
+ name: 'whoami',
75
+ access: 'read',
76
+ description: `Resolve the stable ${config.provider} account identity for local scoping`,
77
+ domain: config.domain,
78
+ strategy: Strategy.COOKIE,
79
+ browser: true,
80
+ ...browserSession,
81
+ navigateBefore: false,
82
+ args: [],
83
+ columns: ['identity'],
84
+ func: async (page) => inTemporaryTab(page, async () => {
85
+ await page.goto(config.home, { settleMs: 500 })
86
+ const result = await evaluate(page, config.whoamiScript, {}, `${config.site} whoami`)
87
+ if (result?.code === 'AUTH') throw new AuthRequiredError(config.domain)
88
+ if (result?.ok !== true || typeof result.identity !== 'string' || !result.identity) {
89
+ throw new CommandExecutionError(`${config.site} whoami: ${result?.message || 'stable account identity unavailable'}`)
90
+ }
91
+ return [{ identity: result.identity }]
92
+ }),
93
+ })
94
+
95
+ cli({
96
+ site: config.site,
97
+ name: 'sync-index',
98
+ access: 'read',
99
+ description: `Resolve the ${config.provider} account and list its conversation history in one browser session`,
100
+ domain: config.domain,
101
+ strategy: Strategy.COOKIE,
102
+ browser: true,
103
+ ...browserSession,
104
+ navigateBefore: false,
105
+ args: [{
106
+ name: 'since',
107
+ help: 'ISO 8601 instant; stop paging once the listing predates it. Honoured only while the provider pages newest-first',
108
+ }, { name: 'accountScope', help: 'Previously observed hashed account scope; mismatch forces a full listing' }],
109
+ columns: SYNC_INDEX_COLUMNS,
110
+ func: async (page, kwargs) => inTemporaryTab(page, async () => {
111
+ const since = String(kwargs.since || '').trim()
112
+ if (since && Number.isNaN(Date.parse(since))) throw new ArgumentError('since must be an ISO 8601 instant')
113
+ await page.goto(config.home, { settleMs: 1200 })
114
+ const identity = await evaluate(page, config.whoamiScript, {}, `${config.site} sync-index identity`)
115
+ if (identity?.code === 'AUTH') throw new AuthRequiredError(config.domain)
116
+ if (identity?.ok !== true || typeof identity.identity !== 'string' || !identity.identity) {
117
+ throw new CommandExecutionError(`${config.site} sync-index: ${identity?.message || 'stable account identity unavailable'}`)
118
+ }
119
+ const accountScope = createHash('sha256').update(`${config.provider.toLocaleLowerCase()}\0${identity.identity}`).digest('hex')
120
+ const effectiveSince = String(kwargs.accountScope || '') === accountScope ? since : ''
121
+ const rows = assertResult(
122
+ await evaluate(page, config.historyScript, { since: effectiveSince }, `${config.site} sync-index history`),
123
+ config.domain,
124
+ `${config.site} sync-index`,
125
+ )
126
+ return [
127
+ { kind: 'identity', identity: identity.identity, sinceApplied: effectiveSince },
128
+ ...rows.map(row => ({ kind: 'conversation', identity: '', sinceApplied: '', ...row })),
129
+ ]
130
+ }),
131
+ })
132
+
133
+ cli({
134
+ site: config.site,
135
+ name: 'history-all',
136
+ access: 'read',
137
+ description: `List the ${config.provider} web conversation history, whole or since an instant`,
138
+ domain: config.domain,
139
+ strategy: Strategy.COOKIE,
140
+ browser: true,
141
+ ...browserSession,
142
+ navigateBefore: false,
143
+ args: [{
144
+ name: 'since',
145
+ help: 'ISO 8601 instant; stop paging once the listing predates it. Honoured only while the provider pages newest-first',
146
+ }],
147
+ columns: HISTORY_COLUMNS,
148
+ func: async (page, kwargs) => inTemporaryTab(page, async () => {
149
+ const since = String(kwargs.since || '').trim()
150
+ if (since && Number.isNaN(Date.parse(since))) throw new ArgumentError('since must be an ISO 8601 instant')
151
+ await page.goto(config.home, { settleMs: 1200 })
152
+ const rows = assertResult(
153
+ await evaluate(page, config.historyScript, { since }, `${config.site} history-all`),
154
+ config.domain,
155
+ `${config.site} history-all`,
156
+ )
157
+ return rows
158
+ }),
159
+ })
160
+
161
+ cli({
162
+ site: config.site,
163
+ name: 'detail',
164
+ access: 'read',
165
+ description: `Read one complete ${config.provider} conversation`,
166
+ domain: config.domain,
167
+ strategy: Strategy.COOKIE,
168
+ browser: true,
169
+ ...browserSession,
170
+ navigateBefore: false,
171
+ args: [
172
+ { name: 'id', positional: true, required: true, help: 'Provider conversation id' },
173
+ { name: 'accountScope', help: 'Expected hashed account scope; mismatch refuses the read' },
174
+ ],
175
+ columns: DETAIL_COLUMNS,
176
+ func: async (page, kwargs) => inTemporaryTab(page, async () => {
177
+ const id = String(kwargs.id || '').trim()
178
+ if (!id) throw new ArgumentError('id must be a non-empty conversation id')
179
+ await page.goto(config.home, { settleMs: 600 })
180
+ const expectedScope = String(kwargs.accountScope || '').trim()
181
+ if (expectedScope) {
182
+ const identity = await evaluate(page, config.whoamiScript, {}, `${config.site} detail identity`)
183
+ if (identity?.code === 'AUTH') throw new AuthRequiredError(config.domain)
184
+ if (identity?.ok !== true || typeof identity.identity !== 'string' || !identity.identity) {
185
+ throw new CommandExecutionError(`${config.site} detail: stable account identity unavailable`)
186
+ }
187
+ const actualScope = createHash('sha256').update(`${config.provider.toLocaleLowerCase()}\0${identity.identity}`).digest('hex')
188
+ if (actualScope !== expectedScope) {
189
+ throw new CommandExecutionError(`DSH_ACCOUNT_SCOPE_MISMATCH: ${config.site} detail: conversation belongs to a different logged-in account`)
190
+ }
191
+ }
192
+ let result = await evaluate(page, config.detailScript, { id }, `${config.site} detail`)
193
+ if (result?.ok !== true && result?.code !== 'AUTH' && result?.code !== 'RATE_LIMIT' && config.fallbackScript) {
194
+ await page.goto(config.conversationUrl(id), { settleMs: 1800 })
195
+ result = await evaluate(page, config.fallbackScript, { id }, `${config.site} detail fallback`)
196
+ }
197
+ const rows = assertResult(result, config.domain, `${config.site} detail`)
198
+ if (rows.length === 0) throw new EmptyResultError(`${config.site} detail`, `No messages found for ${id}`)
199
+ return rows
200
+ }),
201
+ })
202
+
203
+ cli({
204
+ site: config.site,
205
+ name: 'attachment',
206
+ access: 'read',
207
+ description: `Materialize one authorized ${config.provider} attachment`,
208
+ domain: config.domain,
209
+ strategy: Strategy.COOKIE,
210
+ browser: true,
211
+ ...browserSession,
212
+ navigateBefore: false,
213
+ args: [
214
+ { name: 'locator', positional: true, required: true, help: 'Stable same-origin attachment locator' },
215
+ { name: 'output', required: true, help: 'Absolute output file path' },
216
+ { name: 'maxBytes', type: 'int', default: 26214400, help: 'Maximum decoded bytes' },
217
+ ],
218
+ columns: ['attachmentId', 'name', 'mimeType', 'size', 'status', 'localPath'],
219
+ func: async (page, kwargs) => inTemporaryTab(page, async () => {
220
+ const locator = String(kwargs.locator || '').trim()
221
+ const output = resolve(String(kwargs.output || ''))
222
+ const maxBytes = Number(kwargs.maxBytes ?? 26_214_400)
223
+ if (!locator) throw new ArgumentError('locator must not be empty')
224
+ if (!Number.isInteger(maxBytes) || maxBytes < 1 || maxBytes > 26_214_400) {
225
+ throw new ArgumentError('maxBytes must be between 1 and 26214400')
226
+ }
227
+ await page.goto(config.home, { settleMs: 500 })
228
+ const result = await evaluate(page, ATTACHMENT_SCRIPT, {
229
+ locator,
230
+ origin: new URL(config.home).origin,
231
+ maxBytes,
232
+ }, `${config.site} attachment`)
233
+ if (result?.code === 'AUTH') throw new AuthRequiredError(config.domain)
234
+ if (result?.ok !== true || typeof result.base64 !== 'string') {
235
+ throw new CommandExecutionError(`${config.site} attachment: ${result?.message || 'attachment unavailable'}`)
236
+ }
237
+ const bytes = Buffer.from(result.base64, 'base64')
238
+ if (bytes.byteLength > maxBytes) throw new CommandExecutionError(`${config.site} attachment exceeds maxBytes`)
239
+ await mkdir(dirname(output), { recursive: true })
240
+ await writeFile(output, bytes)
241
+ return [{
242
+ attachmentId: locator,
243
+ name: result.name || output.split(/[\\/]/).pop() || 'attachment',
244
+ mimeType: result.mimeType || 'application/octet-stream',
245
+ size: bytes.byteLength,
246
+ status: 'available',
247
+ localPath: output,
248
+ }]
249
+ }),
250
+ })
251
+ }
252
+
253
+ /** Browser Bridge lifecycle contract shared by every web-chat sync command. */
254
+ export const SYNC_BROWSER_SESSION = Object.freeze({
255
+ siteSession: 'ephemeral',
256
+ defaultWindowMode: 'background',
257
+ })
258
+
259
+ const ATTACHMENT_SCRIPT = String.raw`async function (args) {
260
+ try {
261
+ const target = new URL(args.locator, args.origin)
262
+ if (target.origin !== args.origin) return JSON.stringify({ ok: false, message: 'cross-origin locator refused' })
263
+ const response = await fetch(target.href, { credentials: 'include' })
264
+ if (response.status === 401 || response.status === 403) return JSON.stringify({ ok: false, code: 'AUTH' })
265
+ if (!response.ok) return JSON.stringify({ ok: false, message: 'attachment HTTP ' + response.status })
266
+ const length = Number(response.headers.get('content-length') || 0)
267
+ if (length > args.maxBytes) return JSON.stringify({ ok: false, message: 'attachment exceeds maxBytes' })
268
+ const bytes = new Uint8Array(await response.arrayBuffer())
269
+ if (bytes.byteLength > args.maxBytes) return JSON.stringify({ ok: false, message: 'attachment exceeds maxBytes' })
270
+ let binary = ''
271
+ for (let at = 0; at < bytes.length; at += 0x8000) {
272
+ binary += String.fromCharCode(...bytes.subarray(at, Math.min(bytes.length, at + 0x8000)))
273
+ }
274
+ return JSON.stringify({
275
+ ok: true,
276
+ base64: btoa(binary),
277
+ mimeType: response.headers.get('content-type') || 'application/octet-stream',
278
+ name: target.pathname.split('/').pop() || 'attachment',
279
+ })
280
+ } catch (error) {
281
+ return JSON.stringify({ ok: false, message: String(error && error.message || error) })
282
+ }
283
+ }`
284
+
285
+ export const domFallbackScript = String.raw`async function (args) {
286
+ const nodes = Array.from(document.querySelectorAll('[data-message-author-role], user-query, model-response, [class*="message"]'))
287
+ const rows = []
288
+ let ordinal = 0
289
+ for (const node of nodes) {
290
+ if (node.parentElement && node.parentElement.closest('[data-message-author-role], user-query, model-response') !== node) continue
291
+ const attr = (node.getAttribute('data-message-author-role') || '').toLowerCase()
292
+ const tag = node.tagName.toLowerCase()
293
+ const role = attr === 'user' || tag === 'user-query' || /user|human/i.test(node.className || '') ? 'user'
294
+ : attr === 'assistant' || tag === 'model-response' || /assistant|model-response|claude/i.test(node.className || '') ? 'assistant'
295
+ : null
296
+ const text = (node.innerText || node.textContent || '').trim()
297
+ if (!role || !text) continue
298
+ rows.push({
299
+ conversationId: args.id,
300
+ ordinal: ordinal++,
301
+ messageId: node.getAttribute('data-message-id') || '',
302
+ parentId: '', branchId: '', activeBranch: true,
303
+ role, text, createdAt: '', attachmentsJson: '[]', partial: true,
304
+ })
305
+ }
306
+ return JSON.stringify({ ok: rows.length > 0, rows, message: rows.length ? '' : 'DOM fallback found no messages' })
307
+ }`
@@ -0,0 +1,152 @@
1
+ import { domFallbackScript, registerProvider, sinceGuardSource } from './common.js'
2
+ // OpenCLI discovery marker: registerProvider below performs the cli() calls.
3
+
4
+ const historyScript = String.raw`async function (args) {
5
+ const stopEarly = (${sinceGuardSource})(args && args.since)
6
+ const normalize = value => Array.isArray(value) ? value : []
7
+ const headers = (() => {
8
+ try {
9
+ const stored = JSON.parse(localStorage.getItem('userToken') || 'null')
10
+ const token = typeof stored === 'string' ? stored : stored && (stored.value || stored.token)
11
+ return token ? { Accept: 'application/json', Authorization: 'Bearer ' + token } : { Accept: 'application/json' }
12
+ } catch { return { Accept: 'application/json' } }
13
+ })()
14
+ const toDate = value => {
15
+ if (typeof value === 'number' && Number.isFinite(value)) return new Date(value < 1e12 ? value * 1000 : value).toISOString()
16
+ return String(value || '')
17
+ }
18
+ const rows = []
19
+ let cursorPinned = false
20
+ let cursorUpdatedAt = ''
21
+ let guard = 0
22
+ try {
23
+ do {
24
+ const query = new URLSearchParams({ count: '100', 'lte_cursor.pinned': String(cursorPinned) })
25
+ if (cursorUpdatedAt) query.set('lte_cursor.updated_at', cursorUpdatedAt)
26
+ const response = await fetch('/api/v0/chat_session/fetch_page?' + query, { credentials: 'include', headers })
27
+ if (response.status === 401 || response.status === 403) return JSON.stringify({ ok: false, code: 'AUTH' })
28
+ if (response.status === 429) return JSON.stringify({ ok: false, code: 'RATE_LIMIT' })
29
+ if (!response.ok) throw new Error('history HTTP ' + response.status)
30
+ const payload = await response.json()
31
+ if (payload && (payload.code === 40002 || /missing token|unauth/i.test(String(payload.msg || payload.message || '')))) {
32
+ return JSON.stringify({ ok: false, code: 'AUTH' })
33
+ }
34
+ const data = payload.data && payload.data.biz_data || payload.biz_data || payload.data || payload
35
+ const items = normalize(data.chat_sessions || data.sessions || data.items || data.conversations)
36
+ const pageStart = rows.length
37
+ for (const item of items) {
38
+ const id = String(item.id || item.chat_session_id || item.uuid || '')
39
+ if (!id) continue
40
+ rows.push({
41
+ provider: 'deepseek', accountScope: '', id,
42
+ title: String(item.title || item.name || 'Untitled DeepSeek chat'),
43
+ url: location.origin + '/a/chat/s/' + encodeURIComponent(id),
44
+ createdAt: toDate(item.inserted_at || item.created_at || item.createdAt || ''),
45
+ updatedAt: toDate(item.updated_at || item.updatedAt || item.inserted_at || item.created_at || ''),
46
+ messageCount: Number(item.message_count || item.messageCount || 0),
47
+ cursor: String(item.updated_at || item.updatedAt || ''), partial: false,
48
+ })
49
+ }
50
+ const last = items[items.length - 1]
51
+ const nextUpdatedAt = String(last && (last.updated_at || last.updatedAt) || '')
52
+ const nextPinned = Boolean(last && last.pinned)
53
+ if (!data.has_more || !nextUpdatedAt || (nextUpdatedAt === cursorUpdatedAt && nextPinned === cursorPinned)) break
54
+ if (stopEarly(rows.slice(pageStart))) break
55
+ cursorUpdatedAt = nextUpdatedAt
56
+ cursorPinned = nextPinned
57
+ } while (++guard < 500)
58
+ return JSON.stringify({ ok: true, rows })
59
+ } catch (error) {
60
+ const links = Array.from(document.querySelectorAll('a[href*="/a/chat/s/"]'))
61
+ const seen = new Set()
62
+ for (const link of links) {
63
+ const match = link.href.match(/\/a\/chat\/s\/([^/?#]+)/)
64
+ if (!match || seen.has(match[1])) continue
65
+ seen.add(match[1])
66
+ rows.push({ provider: 'deepseek', accountScope: '', id: match[1],
67
+ title: (link.textContent || '').trim() || 'Untitled DeepSeek chat', url: link.href,
68
+ createdAt: '', updatedAt: '', messageCount: 0, cursor: '', partial: true })
69
+ }
70
+ return JSON.stringify({ ok: rows.length > 0, rows, message: String(error && error.message || error) })
71
+ }
72
+ }`
73
+
74
+ const detailScript = String.raw`async function (args) {
75
+ const headers = (() => {
76
+ try {
77
+ const stored = JSON.parse(localStorage.getItem('userToken') || 'null')
78
+ const token = typeof stored === 'string' ? stored : stored && (stored.value || stored.token)
79
+ return token ? { Accept: 'application/json', Authorization: 'Bearer ' + token } : { Accept: 'application/json' }
80
+ } catch { return { Accept: 'application/json' } }
81
+ })()
82
+ const toText = value => typeof value === 'string' ? value
83
+ : Array.isArray(value) ? value.map(toText).filter(Boolean).join('\n')
84
+ : value && typeof value === 'object' ? toText(value.text || value.content || value.value || '') : ''
85
+ try {
86
+ const response = await fetch('/api/v0/chat/history_messages?chat_session_id=' + encodeURIComponent(args.id), { credentials: 'include', headers })
87
+ if (response.status === 401 || response.status === 403) return JSON.stringify({ ok: false, code: 'AUTH' })
88
+ if (response.status === 429) return JSON.stringify({ ok: false, code: 'RATE_LIMIT' })
89
+ if (!response.ok) throw new Error('detail HTTP ' + response.status)
90
+ const payload = await response.json()
91
+ if (payload && (payload.code === 40002 || /missing token|unauth/i.test(String(payload.msg || payload.message || '')))) {
92
+ return JSON.stringify({ ok: false, code: 'AUTH' })
93
+ }
94
+ const data = payload.data && payload.data.biz_data || payload.biz_data || payload.data || payload
95
+ const messages = data.chat_messages || data.messages || data.items || []
96
+ const rows = messages.map((message, ordinal) => {
97
+ const roleRaw = String(message.role || message.message_role || message.sender || '').toLowerCase()
98
+ const role = /assistant|bot|model/.test(roleRaw) ? 'assistant' : 'user'
99
+ const files = message.attachments || message.files || message.file_infos || []
100
+ const attachments = (Array.isArray(files) ? files : []).map((file, index) => {
101
+ const candidate = String(file.download_url || file.url || file.path || '')
102
+ let locator = ''
103
+ try { if (candidate) { const url = new URL(candidate, location.origin); if (url.origin === location.origin && url.pathname !== '/') locator = url.pathname + url.search } } catch {}
104
+ const name = String(file.name || file.file_name || 'attachment')
105
+ const mimeType = String(file.mime_type || file.mimeType || '')
106
+ return { attachmentId: String(file.id || file.file_id || index),
107
+ kind: mimeType.startsWith('image/') || /\.(?:gif|jpe?g|png|webp)$/i.test(name) ? 'image' : 'file',
108
+ name, mimeType,
109
+ size: Number(file.size || file.file_size || 0), locator, status: locator ? 'available' : 'unavailable' }
110
+ })
111
+ return { conversationId: args.id, ordinal,
112
+ messageId: String(message.id || message.message_id || ordinal),
113
+ parentId: String(message.parent_id || message.parentId || ''), branchId: '', activeBranch: true,
114
+ role, text: toText(message.content || message.text || message.fragments || ''),
115
+ createdAt: String(message.created_at || message.createdAt || ''),
116
+ attachmentsJson: JSON.stringify(attachments), partial: false }
117
+ }).filter(row => row.text || row.attachmentsJson !== '[]')
118
+ return JSON.stringify({ ok: rows.length > 0, rows })
119
+ } catch (error) {
120
+ return JSON.stringify({ ok: false, message: String(error && error.message || error) })
121
+ }
122
+ }`
123
+
124
+ registerProvider({
125
+ site: 'dsh-deepseek', provider: 'DeepSeek', domain: 'chat.deepseek.com',
126
+ home: 'https://chat.deepseek.com/', conversationUrl: id => `https://chat.deepseek.com/a/chat/s/${encodeURIComponent(id)}`,
127
+ whoamiScript: String.raw`async function () {
128
+ const headers = (() => {
129
+ try {
130
+ const stored = JSON.parse(localStorage.getItem('userToken') || 'null')
131
+ const token = typeof stored === 'string' ? stored : stored && (stored.value || stored.token)
132
+ return token ? { Accept: 'application/json', Authorization: 'Bearer ' + token } : { Accept: 'application/json' }
133
+ } catch { return { Accept: 'application/json' } }
134
+ })()
135
+ for (const url of ['/api/v0/users/current', '/api/v0/user/profile']) {
136
+ const response = await fetch(url, { credentials: 'include', headers })
137
+ if (response.status === 401 || response.status === 403) return JSON.stringify({ ok: false, code: 'AUTH' })
138
+ if (!response.ok) continue
139
+ const contentType = response.headers.get('content-type') || ''
140
+ if (!contentType.includes('application/json')) continue
141
+ const payload = await response.json()
142
+ if (payload && (payload.code === 40002 || /missing token|unauth/i.test(String(payload.msg || payload.message || '')))) {
143
+ return JSON.stringify({ ok: false, code: 'AUTH' })
144
+ }
145
+ const user = payload.data && payload.data.biz_data || payload.biz_data || payload.data || payload
146
+ const identity = String(user.id || user.user_id || user.uuid || user.email || '')
147
+ if (identity) return JSON.stringify({ ok: true, identity })
148
+ }
149
+ return JSON.stringify({ ok: false, code: 'AUTH' })
150
+ }`,
151
+ historyScript, detailScript, fallbackScript: domFallbackScript,
152
+ })