dsh-long-archive 2.0.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 +21 -0
- package/README.md +101 -0
- package/cordis.patch.yml +13 -0
- package/lib/client.js +1162 -0
- package/lib/index.js +327 -0
- package/package.json +65 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// Archived-session manager — host half (v2.1).
|
|
2
|
+
//
|
|
3
|
+
// Transport: the official connection RPC channel (`/arcv`, loopback
|
|
4
|
+
// authority) — the platform fence (loopback + Origin/Host + sec-fetch-site)
|
|
5
|
+
// replaces the hand-rolled /__arcv route and its three CSRF layers.
|
|
6
|
+
//
|
|
7
|
+
// Delete pipeline. Common (idle/no-agent) path, fully synchronous in the
|
|
8
|
+
// request: bounded agent teardown → durable log rm → workspace detach →
|
|
9
|
+
// archive-list removal. The log rm runs BEFORE any accounting change, so the
|
|
10
|
+
// session can never surface as a live "ungrouped" row — it vanishes
|
|
11
|
+
// atomically. Busy-agent path: the request returns ok once the teardown has
|
|
12
|
+
// been started (bounded), and a background chain finishes the same order —
|
|
13
|
+
// rm first, de-archive LAST — after the agent's final write-behind events
|
|
14
|
+
// flush (settle + grace), so the persistence coordinator's retirement runs
|
|
15
|
+
// against a live file instead of ENOENT-ing into a permanent leak. Until the
|
|
16
|
+
// chain completes the session stays in archivedSessionIds (hidden), never
|
|
17
|
+
// visible as active.
|
|
18
|
+
import { rm } from 'node:fs/promises'
|
|
19
|
+
import { dirname, parse, resolve } from 'node:path'
|
|
20
|
+
|
|
21
|
+
export const name = 'archive-manager'
|
|
22
|
+
|
|
23
|
+
export const inject = ['connection', 'agents', 'sessionPersistence', 'workspaceRegistry', 'storageDomain', 'sessions']
|
|
24
|
+
|
|
25
|
+
export function apply(ctx) {
|
|
26
|
+
// 防崩:任何加载期异常都只记录日志,绝不抛出——避免 DSH 起不来。
|
|
27
|
+
try {
|
|
28
|
+
const agentDisposers = new Map()
|
|
29
|
+
const agents = ctx.agents
|
|
30
|
+
|
|
31
|
+
// Teardown budget for a busy agent (whenIdle waits for its current turn to
|
|
32
|
+
// settle — unbounded by nature). An idle agent disposes in ms, so the race
|
|
33
|
+
// rarely binds; after the budget the delete switches to the background
|
|
34
|
+
// chain instead of blocking the request.
|
|
35
|
+
const TEARDOWN_WAIT_MS = 1000
|
|
36
|
+
// Grace after a settled teardown before the log rm: the persistence
|
|
37
|
+
// coordinator retires (flushing the agent's final events) shortly AFTER
|
|
38
|
+
// the disposer resolves; the write-behind drain is immediate once
|
|
39
|
+
// barrier-flushed. Half a second covers dispatch + flush with margin.
|
|
40
|
+
const RM_GRACE_MS = 500
|
|
41
|
+
|
|
42
|
+
// ── AgentHandle.dispose capture ──
|
|
43
|
+
// rc.6 has no agents.stop and agent/created carries no disposal capability,
|
|
44
|
+
// so wrapping the registry entry points is the only capture surface. Covers
|
|
45
|
+
// every agent created or resumed through ctx.agents; config-declared agents
|
|
46
|
+
// (agent-loop's internal create path) bypass this — their deletion reports
|
|
47
|
+
// no-captured-disposer and needs a restart, by design.
|
|
48
|
+
const origCreate = agents.create.bind(agents)
|
|
49
|
+
const origResume = agents.resume.bind(agents)
|
|
50
|
+
const disposeWrapper = ctx.effect(() => {
|
|
51
|
+
agents.create = async (options) => {
|
|
52
|
+
const handle = await origCreate(options)
|
|
53
|
+
try { agentDisposers.set(handle.agent.id, handle.dispose) } catch (e) {}
|
|
54
|
+
return handle
|
|
55
|
+
}
|
|
56
|
+
agents.resume = async (options) => {
|
|
57
|
+
const handle = await origResume(options)
|
|
58
|
+
try { agentDisposers.set(handle.agent.id, handle.dispose) } catch (e) {}
|
|
59
|
+
return handle
|
|
60
|
+
}
|
|
61
|
+
return () => {
|
|
62
|
+
agents.create = origCreate
|
|
63
|
+
agents.resume = origResume
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
function archivedIds() {
|
|
68
|
+
const state = ctx.workspaceRegistry.state
|
|
69
|
+
const ids = state !== undefined ? state.archivedSessionIds : undefined
|
|
70
|
+
return Array.isArray(ids) ? ids : []
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Serialized read-modify-write on the registry's own operation chain: the
|
|
74
|
+
// registry's canonical mutations (create/delete/order) are also non-atomic
|
|
75
|
+
// multi-step domain writes, so an independent plugin-side chain could
|
|
76
|
+
// interleave and silently roll archivedSessionIds back. enqueueOperation
|
|
77
|
+
// puts this mutation on the same mutual-exclusion chain as every official
|
|
78
|
+
// write; the mirror write reuses the registry's own state reference.
|
|
79
|
+
function mutateArchived(update) {
|
|
80
|
+
return ctx.workspaceRegistry.enqueueOperation(async () => {
|
|
81
|
+
const domain = ctx.storageDomain.get('workspace')
|
|
82
|
+
const state = domain.global.get()
|
|
83
|
+
const archived = Array.isArray(state.archivedSessionIds) ? state.archivedSessionIds : []
|
|
84
|
+
const nextIds = update(archived)
|
|
85
|
+
if (nextIds === archived) return
|
|
86
|
+
await domain.global.set({ ...state, archivedSessionIds: nextIds })
|
|
87
|
+
const registry = ctx.workspaceRegistry
|
|
88
|
+
if (registry.state !== undefined) registry.state.archivedSessionIds = nextIds
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function handleUnarchive(args) {
|
|
93
|
+
const id = args.sessionId
|
|
94
|
+
if (typeof id !== 'string' || id === '') return { ok: false, code: 'bad-session-id' }
|
|
95
|
+
if (!archivedIds().includes(id)) return { ok: false, code: 'not-archived' }
|
|
96
|
+
// A delete whose removal is in flight — the sync pipeline (from its first
|
|
97
|
+
// step until finalize lands) or the busy-agent background chain — must
|
|
98
|
+
// finish unconditionally, so an unarchive here would resurrect a session
|
|
99
|
+
// the delete then silently destroys. Refuse until the entry is cleared.
|
|
100
|
+
if (pendingFinalize.has(id)) return { ok: false, code: 'delete-pending' }
|
|
101
|
+
await mutateArchived((archived) => (archived.includes(id) ? archived.filter((x) => x !== id) : archived))
|
|
102
|
+
return { ok: true }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── durable log removal ──
|
|
106
|
+
// findLog (async!) probes each project directory with existence checks
|
|
107
|
+
// only (no header parsing), including the cwd-less `_no-cwd` bucket. It is
|
|
108
|
+
// a jsonl-backend member, so guard with typeof. Resolves undefined when no
|
|
109
|
+
// log exists — already gone counts as removed.
|
|
110
|
+
//
|
|
111
|
+
// Windows transient-lock retry: `force: true` only suppresses ENOENT — it
|
|
112
|
+
// never hides EBUSY/EPERM. On Windows those surface when the tree is
|
|
113
|
+
// momentarily held (just-closed handle teardown, Defender scan, SMB/9p
|
|
114
|
+
// remaps like \\wsl.localhost), so a single-shot rm used to fail the whole
|
|
115
|
+
// delete with `log-removal-failed`. The short retry ladder (~1s total)
|
|
116
|
+
// absorbs those windows; anything else still fails fast.
|
|
117
|
+
const REMOVE_RETRY_DELAYS_MS = [0, 120, 300, 600]
|
|
118
|
+
const REMOVE_RETRYABLE_CODES = new Set(['EBUSY', 'EPERM', 'EACCES', 'UNKNOWN', 'ETXTBSY'])
|
|
119
|
+
async function removeLogDir(id) {
|
|
120
|
+
const persistence = ctx.sessionPersistence
|
|
121
|
+
if (persistence === undefined || typeof persistence.findLog !== 'function') return false
|
|
122
|
+
let path
|
|
123
|
+
try { path = await persistence.findLog(id) } catch (e) { return false }
|
|
124
|
+
if (typeof path !== 'string' || path === '') return true
|
|
125
|
+
const dir = resolve(dirname(path))
|
|
126
|
+
// Root guard must be cross-platform: POSIX `/`, Windows drive roots
|
|
127
|
+
// (`C:\`), and UNC share roots all identify themselves as the path root.
|
|
128
|
+
if (parse(dir).root === dir) return false
|
|
129
|
+
for (let attempt = 0; ; attempt++) {
|
|
130
|
+
try {
|
|
131
|
+
await rm(dir, { recursive: true, force: true })
|
|
132
|
+
return true
|
|
133
|
+
} catch (error) {
|
|
134
|
+
const code = error !== null && typeof error === 'object' && typeof error.code === 'string'
|
|
135
|
+
? error.code
|
|
136
|
+
: undefined
|
|
137
|
+
const retriable = code !== undefined && REMOVE_RETRYABLE_CODES.has(code)
|
|
138
|
+
if (!retriable || attempt === REMOVE_RETRY_DELAYS_MS.length - 1) {
|
|
139
|
+
ctx.logger?.warn(`archive-manager: log removal for ${id} failed${code !== undefined ? ` (${code})` : ''}: ${error instanceof Error ? error.message : String(error)}`)
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
await new Promise((resolve2) => setTimeout(resolve2, REMOVE_RETRY_DELAYS_MS[attempt + 1]))
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Accounting AFTER a successful rm: detach from every workspace entity
|
|
148
|
+
// (detachSession is idempotent; calling it unconditionally also cleans
|
|
149
|
+
// durable records the visible sessionIds getter would filter out) and
|
|
150
|
+
// remove the archive flag LAST — the moment the id leaves the archive set
|
|
151
|
+
// it must already be gone from disk, or it would surface as an ungrouped
|
|
152
|
+
// live session.
|
|
153
|
+
async function finalizeRemoval(id) {
|
|
154
|
+
const registry = ctx.workspaceRegistry
|
|
155
|
+
if (registry !== undefined && registry.entities !== undefined && typeof registry.entities.values === 'function') {
|
|
156
|
+
for (const entity of registry.entities.values()) {
|
|
157
|
+
try { await entity.detachSession(id) } catch (e) {}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
await mutateArchived((archived) => (archived.includes(id) ? archived.filter((x) => x !== id) : archived))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Bounded best-effort agent teardown. Both handlers retire the captured
|
|
164
|
+
// entry, so nothing rejects unhandled; a synchronous throw from the
|
|
165
|
+
// disposal call counts as a failed teardown (entry retired, delete
|
|
166
|
+
// proceeds). Returns the settled-teardown promise and whether it settled
|
|
167
|
+
// within the budget, so the caller picks the sync or background pipeline.
|
|
168
|
+
async function teardownAgent(id) {
|
|
169
|
+
const disposer = agentDisposers.get(id)
|
|
170
|
+
if (disposer === undefined) {
|
|
171
|
+
return { state: ctx.agents.get(id) !== undefined ? 'no-captured-disposer' : 'idle', teardown: undefined, settled: true }
|
|
172
|
+
}
|
|
173
|
+
let teardown
|
|
174
|
+
try {
|
|
175
|
+
teardown = disposer()
|
|
176
|
+
} catch (error) {
|
|
177
|
+
agentDisposers.delete(id)
|
|
178
|
+
return { state: 'ok', teardown: undefined, settled: true }
|
|
179
|
+
}
|
|
180
|
+
const settled = teardown.then(
|
|
181
|
+
() => { agentDisposers.delete(id) },
|
|
182
|
+
() => { agentDisposers.delete(id) },
|
|
183
|
+
)
|
|
184
|
+
let settledWithinBudget = false
|
|
185
|
+
let timer
|
|
186
|
+
try {
|
|
187
|
+
await Promise.race([
|
|
188
|
+
settled.then(() => { settledWithinBudget = true }),
|
|
189
|
+
new Promise((resolve2) => { timer = setTimeout(resolve2, TEARDOWN_WAIT_MS) }),
|
|
190
|
+
])
|
|
191
|
+
} finally {
|
|
192
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
193
|
+
}
|
|
194
|
+
return { state: 'ok', teardown: settled, settled: settledWithinBudget }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── background finalization for busy agents ──
|
|
198
|
+
// One chain per id. On any step's failure the session simply stays archived
|
|
199
|
+
// (hidden) with its log on disk — the row may reappear in the archive panel
|
|
200
|
+
// after a refresh, which is the honest state for an unfinished delete.
|
|
201
|
+
const pendingFinalize = new Map()
|
|
202
|
+
function scheduleFinalization(id, teardown) {
|
|
203
|
+
if (pendingFinalize.has(id)) return
|
|
204
|
+
const entry = { done: false }
|
|
205
|
+
pendingFinalize.set(id, entry)
|
|
206
|
+
const finish = () => {
|
|
207
|
+
if (entry.done) return
|
|
208
|
+
entry.done = true
|
|
209
|
+
pendingFinalize.delete(id)
|
|
210
|
+
removeLogDir(id).then((removed) => {
|
|
211
|
+
if (removed) return finalizeRemoval(id)
|
|
212
|
+
ctx.logger?.warn(`archive-manager: deferred log removal for ${id} failed; session stays archived`)
|
|
213
|
+
}).catch(() => {})
|
|
214
|
+
}
|
|
215
|
+
// The teardown promise never rejects (both handlers attached in
|
|
216
|
+
// teardownAgent); settle → grace → finish.
|
|
217
|
+
teardown.then(() => { setTimeout(finish, RM_GRACE_MS) })
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function handleDelete(args) {
|
|
221
|
+
const id = args.sessionId
|
|
222
|
+
if (typeof id !== 'string' || id === '') return { ok: false, code: 'bad-session-id' }
|
|
223
|
+
// Route-level authorization: only archived members may be deleted (the
|
|
224
|
+
// panel lists exactly these; anything else is a forged request).
|
|
225
|
+
if (!archivedIds().includes(id)) return { ok: false, code: 'not-archived' }
|
|
226
|
+
// Durable removal needs the jsonl findLog surface; without it the delete
|
|
227
|
+
// would strip the archive/workspace accounting and leave the log on disk
|
|
228
|
+
// — fail fast instead of reporting a fake success.
|
|
229
|
+
if (ctx.sessionPersistence === undefined || typeof ctx.sessionPersistence.findLog !== 'function') {
|
|
230
|
+
return { ok: false, code: 'persistence-unavailable' }
|
|
231
|
+
}
|
|
232
|
+
// Live agent: bounded teardown. An agent whose disposer was lost to a
|
|
233
|
+
// host reload is the single hard refusal — only a restart can release it.
|
|
234
|
+
let agentWasLive = false
|
|
235
|
+
let teardown
|
|
236
|
+
let settled = true
|
|
237
|
+
if (ctx.agents.get(id) !== undefined) {
|
|
238
|
+
agentWasLive = true
|
|
239
|
+
const result = await teardownAgent(id)
|
|
240
|
+
if (result.state === 'no-captured-disposer') {
|
|
241
|
+
return { ok: false, code: 'no-captured-disposer' }
|
|
242
|
+
}
|
|
243
|
+
teardown = result.teardown
|
|
244
|
+
settled = result.settled
|
|
245
|
+
}
|
|
246
|
+
// Page-open protection: the client checks its own current session; this
|
|
247
|
+
// host-side recheck covers the gap where the session record is live in
|
|
248
|
+
// another tab/page but its agent already settled here (v1 refused this).
|
|
249
|
+
if (ctx.sessions?.get(id) !== undefined) {
|
|
250
|
+
return { ok: false, code: 'still-live' }
|
|
251
|
+
}
|
|
252
|
+
if (!agentWasLive || settled) {
|
|
253
|
+
// Synchronous pipeline (idle agent or none): the response waits for the
|
|
254
|
+
// real removal, exactly like v1. The grace only covers a settled
|
|
255
|
+
// teardown's in-flight retirement flush. The pipeline registers a
|
|
256
|
+
// pendingFinalize entry for its whole span so a concurrent unarchive in
|
|
257
|
+
// another tab cannot resurrect a session whose log is already gone (the
|
|
258
|
+
// handler refuses delete-pending while it is set).
|
|
259
|
+
const entry = { done: false }
|
|
260
|
+
pendingFinalize.set(id, entry)
|
|
261
|
+
try {
|
|
262
|
+
if (agentWasLive) {
|
|
263
|
+
await new Promise((resolve2) => setTimeout(resolve2, RM_GRACE_MS))
|
|
264
|
+
}
|
|
265
|
+
const removed = await removeLogDir(id)
|
|
266
|
+
if (!removed) return { ok: false, code: 'log-removal-failed' }
|
|
267
|
+
await finalizeRemoval(id)
|
|
268
|
+
return { ok: true }
|
|
269
|
+
} finally {
|
|
270
|
+
pendingFinalize.delete(id)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Busy agent: ok now, chain finishes after the turn ends. The session
|
|
274
|
+
// stays archived (hidden) until the log is actually gone.
|
|
275
|
+
scheduleFinalization(id, teardown)
|
|
276
|
+
return { ok: true }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const dispatch = {
|
|
280
|
+
unarchive: handleUnarchive,
|
|
281
|
+
delete: handleDelete,
|
|
282
|
+
diag: async (args) => {
|
|
283
|
+
const id = args.sessionId
|
|
284
|
+
if (typeof id !== 'string' || !archivedIds().includes(id)) return { ok: false, code: 'not-archived' }
|
|
285
|
+
return {
|
|
286
|
+
agentLive: ctx.agents.get(id) !== undefined,
|
|
287
|
+
sessionLive: ctx.sessions?.get(id) !== undefined,
|
|
288
|
+
captured: agentDisposers.has(id),
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 0.1.5: connection.rpc 对插件不可用(内部 owner.webServer 取不到注入)→ 直接挂 HTTP 路由
|
|
294
|
+
const disposeRoute = ctx.webServer.register({
|
|
295
|
+
kind: 'exact',
|
|
296
|
+
path: '/api/dsh-archive-rpc',
|
|
297
|
+
handler: async (req, res) => {
|
|
298
|
+
try {
|
|
299
|
+
if (req.method !== 'POST') { res.writeHead(405); res.end('method not allowed'); return }
|
|
300
|
+
const chunks = []
|
|
301
|
+
for await (const chunk of req) chunks.push(chunk)
|
|
302
|
+
const body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}')
|
|
303
|
+
const endpoint = String(body.endpoint ?? '')
|
|
304
|
+
const handler = Object.prototype.hasOwnProperty.call(dispatch, endpoint) ? dispatch[endpoint] : undefined
|
|
305
|
+
if (handler === undefined) throw new Error(`unknown endpoint: ${endpoint}`)
|
|
306
|
+
const result = await handler(body.payload ?? {})
|
|
307
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
308
|
+
res.end(JSON.stringify(result ?? {}))
|
|
309
|
+
} catch (error) {
|
|
310
|
+
res.writeHead(400, { 'content-type': 'application/json; charset=utf-8' })
|
|
311
|
+
res.end(JSON.stringify({ ok: false, code: 'bad-request', message: String((error && error.message) || error) }))
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
return () => {
|
|
317
|
+
disposeRoute()
|
|
318
|
+
disposeWrapper()
|
|
319
|
+
for (const entry of pendingFinalize.values()) entry.done = true
|
|
320
|
+
pendingFinalize.clear()
|
|
321
|
+
}
|
|
322
|
+
} catch (error) {
|
|
323
|
+
try { ctx.logger?.warn?.(`archive-manager: apply failed (skipped, DSH unaffected): ${String((error && error.message) || error)}`) } catch (e) {}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export default { name, inject, apply }
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-long-archive",
|
|
3
|
+
"description": "Archived-session manager for DSH Web: sidebar footer entry, grouped archive panel, restore/delete with full agent teardown (DSH 0.1.5-rc.2 适配版)",
|
|
4
|
+
"version": "2.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./client": "./lib/client.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"dsh": {
|
|
13
|
+
"client": {
|
|
14
|
+
"platform": "web",
|
|
15
|
+
"inject": [
|
|
16
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
17
|
+
"@deepseek-ai/dsh-client-locale",
|
|
18
|
+
"@deepseek-ai/dsh-client-connection",
|
|
19
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
20
|
+
"@deepseek-ai/dsh-client-ui-primitives",
|
|
21
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
22
|
+
"@deepseek-ai/dsh-client-ui-layout"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"bundle": {
|
|
26
|
+
"patch": "./cordis.patch.yml"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
31
|
+
"react": "^18.2.0",
|
|
32
|
+
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
|
|
33
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
|
|
34
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
|
|
35
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
36
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
|
|
37
|
+
"@deepseek-ai/dsh-client-ui-workspace": "^0.1.0-rc.6",
|
|
38
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
|
39
|
+
"@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.6",
|
|
40
|
+
"@deepseek-ai/dsh-storage-domain": "^0.1.0-rc.6",
|
|
41
|
+
"@deepseek-ai/dsh-workspace": "^0.1.0-rc.6"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"lib/index.js",
|
|
45
|
+
"lib/client.js",
|
|
46
|
+
"cordis.patch.yml",
|
|
47
|
+
"README.md",
|
|
48
|
+
"LICENSE"
|
|
49
|
+
],
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "git+https://github.com/Jasonrale/dsh-archive-manager.git"
|
|
53
|
+
},
|
|
54
|
+
"keywords": [
|
|
55
|
+
"dsh",
|
|
56
|
+
"deepseek-harness",
|
|
57
|
+
"cordis",
|
|
58
|
+
"plugin",
|
|
59
|
+
"archive"
|
|
60
|
+
],
|
|
61
|
+
"license": "MIT",
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=22"
|
|
64
|
+
}
|
|
65
|
+
}
|