dsh-reference-anything 0.4.3 → 0.5.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 (122) hide show
  1. package/NOTICE.md +58 -0
  2. package/README.md +159 -70
  3. package/cordis.patch.yml +17 -15
  4. package/images/agents/LICENSE +22 -0
  5. package/images/agents/README.md +3 -0
  6. package/images/agents/antigravity.svg +1 -0
  7. package/images/agents/chatgpt.svg +1 -0
  8. package/images/agents/claude.svg +1 -0
  9. package/images/agents/cline.svg +1 -0
  10. package/images/agents/codex.svg +1 -0
  11. package/images/agents/continue.svg +1 -0
  12. package/images/agents/crush.svg +1 -0
  13. package/images/agents/cursor.svg +1 -0
  14. package/images/agents/dsh.svg +1 -0
  15. package/images/agents/gemini.svg +1 -0
  16. package/images/agents/goose.svg +1 -0
  17. package/images/agents/grokbuild.svg +1 -0
  18. package/images/agents/hermes.svg +1 -0
  19. package/images/agents/kilocode.svg +1 -0
  20. package/images/agents/kimi.svg +1 -0
  21. package/images/agents/mimocode.svg +1 -0
  22. package/images/agents/openclaw.svg +1 -0
  23. package/images/agents/opencode.svg +1 -0
  24. package/images/agents/pi.svg +1 -0
  25. package/images/agents/qoder.svg +1 -0
  26. package/images/agents/qwen.svg +1 -0
  27. package/images/agents/reasonix.svg +6 -0
  28. package/images/agents/teleagent.svg +1 -0
  29. package/images/agents/trae.svg +1 -0
  30. package/images/agents/vibe.svg +1 -0
  31. package/images/agents/workbuddy.svg +37 -0
  32. package/images/agents/zcode.svg +1 -0
  33. package/images/agents/zed.svg +1 -0
  34. package/lib/agent-scan-cache.d.ts +13 -0
  35. package/lib/agent-scan-cache.js +43 -0
  36. package/lib/chat-import-compatibility.d.ts +18 -0
  37. package/lib/chat-import-compatibility.js +87 -0
  38. package/lib/chat-import-discovery.d.ts +32 -0
  39. package/lib/chat-import-discovery.js +17 -0
  40. package/lib/chat-import-reader.d.ts +27 -0
  41. package/lib/chat-import-reader.js +111 -0
  42. package/lib/chat-import-runtime.d.ts +4 -0
  43. package/lib/chat-import-runtime.js +4 -0
  44. package/lib/client/index.d.ts +5 -0
  45. package/lib/client.js +4 -33741
  46. package/lib/contract.d.ts +65 -4
  47. package/lib/contract.js +24 -4
  48. package/lib/conversation-availability.d.ts +13 -0
  49. package/lib/conversation-availability.js +76 -0
  50. package/lib/errors.d.ts +2 -2
  51. package/lib/host.d.ts +104 -11
  52. package/lib/host.js +202 -14
  53. package/lib/index.d.ts +3 -1
  54. package/lib/index.js +31 -3
  55. package/lib/local-conversation-reader.d.ts +3 -0
  56. package/lib/local-conversation-reader.js +81 -0
  57. package/lib/local-conversation-scanner.d.ts +18 -0
  58. package/lib/local-conversation-scanner.js +55 -0
  59. package/lib/local-locator.d.ts +14 -0
  60. package/lib/local-locator.js +34 -0
  61. package/lib/local-plugins.d.ts +6 -0
  62. package/lib/local-plugins.js +119 -0
  63. package/lib/local-reference-tools.d.ts +4 -0
  64. package/lib/local-reference-tools.js +195 -0
  65. package/lib/local-reference.d.ts +118 -0
  66. package/lib/local-reference.js +289 -0
  67. package/lib/opencli.d.ts +5 -2
  68. package/lib/opencli.js +88 -64
  69. package/lib/reference.d.ts +5 -3
  70. package/lib/reference.js +167 -72
  71. package/lib/render.d.ts +1 -1
  72. package/lib/render.js +4 -4
  73. package/lib/retain.d.ts +14 -0
  74. package/lib/retain.js +1 -1
  75. package/lib/serialize.d.ts +1 -1
  76. package/lib/serialize.js +2 -2
  77. package/lib/sources/local-agent/index.d.ts +23 -16
  78. package/lib/sources/local-agent/index.js +101 -15
  79. package/lib/store/spec.d.ts +104 -7
  80. package/lib/sync/index.js +4 -0
  81. package/lib/tool.d.ts +2 -0
  82. package/lib/tool.js +36 -41
  83. package/lib/typert.js +3 -0
  84. package/lib/types.d.ts +2 -0
  85. package/lib/update.d.ts +2 -0
  86. package/lib/update.js +12 -0
  87. package/lib/uri-codec.d.ts +1 -0
  88. package/lib/uri-codec.js +64 -3
  89. package/lib/uri.d.ts +1 -10
  90. package/lib/uri.js +16 -5
  91. package/lib/web.js +3 -1
  92. package/lib/wire.d.ts +200 -0
  93. package/lib/wire.js +38 -13
  94. package/opencli-plugin/common.js +5 -3
  95. package/opencli-plugin/opencli-plugin.json +1 -1
  96. package/opencli-plugin/package.json +1 -1
  97. package/package.json +30 -4
  98. package/release-notes/0.4.4/commands.png +0 -0
  99. package/release-notes/0.4.4/demos/agent-import.gif +0 -0
  100. package/release-notes/0.4.4/demos/chat-import-upgrade.gif +0 -0
  101. package/release-notes/0.4.4/demos/folders.gif +0 -0
  102. package/release-notes/0.4.4/demos/local-plugins.gif +0 -0
  103. package/release-notes/0.4.4/demos/shortcuts.gif +0 -0
  104. package/release-notes/0.4.4/features.json +36 -0
  105. package/release-notes/0.4.4/files.png +0 -0
  106. package/release-notes/0.5.0/demos/agent-import.gif +0 -0
  107. package/release-notes/0.5.0/demos/chat-import-upgrade.gif +0 -0
  108. package/release-notes/0.5.0/demos/folders.gif +0 -0
  109. package/release-notes/0.5.0/demos/local-plugins.gif +0 -0
  110. package/release-notes/0.5.0/demos/local-references.gif +0 -0
  111. package/release-notes/0.5.0/demos/shortcuts.gif +0 -0
  112. package/release-notes/0.5.0/features.json +65 -0
  113. package/release-notes/README.md +36 -0
  114. package/vendor/chat-import-reader/LICENSE +22 -0
  115. package/vendor/chat-import-reader/LICENSE.fzstd +21 -0
  116. package/vendor/chat-import-reader/README.md +48 -0
  117. package/vendor/chat-import-reader/availability-worker.mjs +114 -0
  118. package/vendor/chat-import-reader/parsers.d.mts +6 -0
  119. package/vendor/chat-import-reader/parsers.mjs +11168 -0
  120. package/vendor/chat-import-reader/scan-worker.mjs +7 -0
  121. package/vendor/chat-import-reader/scanners.mjs +4286 -0
  122. package/vendor/chat-import-reader/upstream-sha256.json +101 -0
package/lib/host.js CHANGED
@@ -1,9 +1,75 @@
1
+ import { AgentScanCache } from "./agent-scan-cache.js";
2
+ import { localConversationRoots } from "./local-conversation-reader.js";
1
3
  import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
2
4
  import { tmpdir } from 'node:os';
3
5
  import { spawn } from 'node:child_process';
4
6
  import { validateDownloadDirectory } from "./download-directory.js";
7
+ import { resolveLocalPlugins } from "./local-plugins.js";
8
+ import { z } from 'zod';
9
+ import { randomUUID } from 'node:crypto';
10
+ import { discoverySchema, scanFormat, agentKind } from "./chat-import-discovery.js";
11
+ import { ALL_LOCAL_AGENTS, LOCAL_AGENT_LABEL } from "./wire.js";
12
+ import { expandRoot } from "./sources/local-agent/registry.js";
13
+ import { homedir } from 'node:os';
14
+ import { inWorkspace } from "./sources/local-agent/path.js";
15
+ import { chatImportCompatibility, upgradeChatImport } from "./chat-import-compatibility.js";
16
+ const importedItemSchema = z.object({
17
+ status: z.string(), sessionId: z.string().min(1).optional(),
18
+ error: z.string().optional(), reason: z.string().optional(), skipReason: z.string().optional(),
19
+ });
20
+ const chatImportResultSchema = z.discriminatedUnion('mode', [
21
+ importedItemSchema.extend({ mode: z.literal('single') }),
22
+ z.object({ mode: z.literal('batch'), results: z.array(importedItemSchema) }),
23
+ ]);
5
24
  export class ReferenceAnythingRemote extends TypertRemoteService {
25
+ async chatImportStatus() { return chatImportCompatibility(this.ctx); }
26
+ async chatImportUpgradeInstall(signal) {
27
+ await upgradeChatImport(this.ctx, signal);
28
+ return this.chatImportStatus();
29
+ }
30
+ agentScans = new AgentScanCache();
31
+ scannedTargets = new Map();
6
32
  constructor(ctx) { super(ctx, 'referenceAnything'); }
33
+ async agentResume(_agent, input, signal) {
34
+ signal.throwIfAborted();
35
+ this.assertScannedSelection(_agent, input.id);
36
+ const importer = this.ctx.get('chatImport');
37
+ if (typeof importer?.resumeSummary !== 'function')
38
+ throw new Error('Chat Import API unavailable. Open Reference Anything settings → Local Agent conversations to check compatibility and upgrade, then restart DSH.');
39
+ const agents = this.ctx.get('referenceLocalAgents');
40
+ if (!agents)
41
+ throw new Error('Local agent conversations are unavailable.');
42
+ const { format, path } = await agents.importTarget(input.id);
43
+ if (format !== 'claude' && format !== 'codex')
44
+ throw new Error('Chat Import Resume supports Claude Code and Codex only.');
45
+ signal.throwIfAborted();
46
+ const result = await importer.resumeSummary({ format, path }, signal);
47
+ return z.object({ summary: z.string().min(1) }).parse(result).summary;
48
+ }
49
+ async agentImport(_agent, input, signal) {
50
+ signal.throwIfAborted();
51
+ const importer = this.ctx.get('chatImport');
52
+ if (typeof importer?.importPath !== 'function')
53
+ throw new Error('Chat Import API unavailable. Open Reference Anything settings → Local Agent conversations to check compatibility and upgrade, then restart DSH.');
54
+ const agents = this.ctx.get('referenceLocalAgents');
55
+ if (!agents)
56
+ throw new Error('Local agent conversations are unavailable.');
57
+ const scanned = input.id.startsWith('scan:') || input.id.includes(':chat-import:')
58
+ ? this.scannedTargets.get(`${_agent.session.header.id}:${input.id}`) : undefined;
59
+ this.assertScannedSelection(_agent, input.id);
60
+ const target = scanned ? { format: scanned.format, path: scanned.path, sessionIds: scanned.sessionIds } : await agents.importTarget(input.id);
61
+ signal.throwIfAborted();
62
+ const result = await importer.importPath(target, signal);
63
+ const parsed = chatImportResultSchema.parse(result);
64
+ const items = parsed.mode === 'single' ? [parsed] : parsed.results;
65
+ if (items.length !== 1)
66
+ throw new Error('Expected exactly one imported conversation; no conversation was opened.');
67
+ const item = items[0];
68
+ if (!['imported', 'replaced', 'already-imported', 'appended'].includes(item.status) || !item.sessionId) {
69
+ throw new Error(`Conversation was not imported (${item.status}): ${item.error ?? item.reason ?? item.skipReason ?? ''}`);
70
+ }
71
+ return item.sessionId;
72
+ }
7
73
  /**
8
74
  * Discovery for the `@` menu's local-agent group.
9
75
  *
@@ -17,26 +83,144 @@ export class ReferenceAnythingRemote extends TypertRemoteService {
17
83
  const agents = this.ctx.get('referenceLocalAgents');
18
84
  if (agents === undefined)
19
85
  return [];
20
- // A session with no cwd of its own is rare and has no workspace to scope
21
- // to; falling back to the process's directory is the same guess the
22
- // `ReferenceSource.list` seam makes, rather than an empty group.
23
- const rows = await agents.listForWorkspace(input.query, input.limit, agent.session.header.cwd ?? process.cwd(), signal);
24
- return rows.map(row => ({
25
- id: row.ref.id,
26
- kind: row.ref.id.slice(0, row.ref.id.indexOf(':')),
27
- label: row.label,
28
- provider: row.provider ?? '',
29
- ...row.updatedAt === undefined ? {} : { updatedAt: row.updatedAt },
30
- ...row.searchIncomplete === true ? { searchIncomplete: true } : {},
31
- }));
86
+ const importer = this.ctx.get('chatImport');
87
+ {
88
+ const settings = this.ctx.referenceChatHistory.getSettings();
89
+ agents.setAgentDirectories(settings.agentDirectories ?? {});
90
+ const policy = agents.discoveryPolicy();
91
+ const requests = policy.roots.map(root => ({ format: scanFormat(root.kind), path: root.path }));
92
+ {
93
+ for (const kind of ALL_LOCAL_AGENTS) {
94
+ if (!(settings.enabledAgents ?? ALL_LOCAL_AGENTS).includes(kind))
95
+ continue;
96
+ const format = scanFormat(kind);
97
+ requests.push({ format });
98
+ const custom = expandRoot(settings.agentDirectories?.[kind] ?? '', homedir());
99
+ if (custom)
100
+ requests.push({ format, path: custom });
101
+ }
102
+ }
103
+ const candidates = [];
104
+ const discovered = [];
105
+ const warnings = [];
106
+ const seen = new Set();
107
+ const owner = agent.session.header.id;
108
+ // Scope before scanning and deduplicate explicit roots repeated by settings.
109
+ const scannedRequests = new Set();
110
+ for (const request of requests) {
111
+ const kind = agentKind(request.format);
112
+ if ((input.kind && kind !== input.kind) || !(settings.enabledAgents ?? ALL_LOCAL_AGENTS).includes(kind))
113
+ continue;
114
+ const requestKey = JSON.stringify([request.format, request.path ?? null]);
115
+ if (scannedRequests.has(requestKey))
116
+ continue;
117
+ scannedRequests.add(requestKey);
118
+ signal.throwIfAborted();
119
+ try {
120
+ const result = await agents.scanner.scanDiscover(request, signal);
121
+ for (const warning of result.warnings)
122
+ warnings.push({ id: `warning:${warnings.length}`, kind: 'scan-warning', label: warning.format, provider: '', warning: `${warning.target}: ${warning.error}` });
123
+ for (const row of result.sessions) {
124
+ const query = input.query.trim().toLowerCase();
125
+ if (query && ![row.title, row.project, row.sourcePath].some(value => value?.toLowerCase().includes(query)))
126
+ continue;
127
+ const key = JSON.stringify([row.format, row.sourcePath, row.sessionId]);
128
+ if ((input.kind && agentKind(row.format) !== input.kind) || seen.has(key) || (policy.scope === 'workspace' && !inWorkspace(row.cwd ?? '', agent.session.header.cwd ?? process.cwd())))
129
+ continue;
130
+ seen.add(key);
131
+ discovered.push({ ...row });
132
+ }
133
+ }
134
+ catch (error) {
135
+ signal.throwIfAborted();
136
+ warnings.push({ id: `warning:${warnings.length}`, kind: 'scan-warning', label: request.format, provider: '', warning: error instanceof Error ? error.message : String(error) });
137
+ }
138
+ }
139
+ if (importer?.scanDiscover) {
140
+ const statuses = new Map();
141
+ await Promise.all([...scannedRequests].map(async (key) => {
142
+ const [format, path] = JSON.parse(key);
143
+ try {
144
+ const result = await this.agentScans.read(importer, '', key, () => importer.scanDiscover({ format, ...(path ? { path } : {}) }, AbortSignal.timeout(2_000)), AbortSignal.any([signal, AbortSignal.timeout(2_000)]));
145
+ for (const row of result.sessions)
146
+ statuses.set(JSON.stringify([row.format, row.sourcePath, row.sessionId]), row.importStatus);
147
+ }
148
+ catch {
149
+ signal.throwIfAborted();
150
+ }
151
+ }));
152
+ for (const row of discovered)
153
+ row.importStatus = statuses.get(JSON.stringify([row.format, row.sourcePath, row.sessionId]));
154
+ }
155
+ discovered.sort((a, b) => (b.lastActiveAt ?? 0) - (a.lastActiveAt ?? 0));
156
+ for (const row of discovered) {
157
+ signal.throwIfAborted();
158
+ if (candidates.length >= input.limit)
159
+ break;
160
+ if (!await agents.conversationAvailability.visible({ format: row.format, path: row.sourcePath, sessionId: row.sessionId }))
161
+ continue;
162
+ const kind = agentKind(row.format);
163
+ const referenceId = await agents.referenceForDiscovered(kind, row.sourcePath, row.sessionId);
164
+ if (!referenceId && settings.picker?.agents.showImport === false)
165
+ continue;
166
+ const id = referenceId ?? `scan:${randomUUID()}`;
167
+ if (!referenceId || referenceId.includes(':chat-import:')) {
168
+ this.scannedTargets.set(`${owner}:${id}`, { owner, policy: JSON.stringify(settings), format: row.format, path: row.sourcePath, sessionIds: [row.sessionId] });
169
+ if (this.scannedTargets.size > 2000)
170
+ this.scannedTargets.delete(this.scannedTargets.keys().next().value);
171
+ }
172
+ candidates.push({ id, kind, label: row.title || row.sessionId, provider: LOCAL_AGENT_LABEL[kind] ?? row.format,
173
+ ...(referenceId ? { referenceId } : { importOnly: true }), importStatus: row.importStatus,
174
+ ...(row.lastActiveAt == null ? {} : { updatedAt: row.lastActiveAt }) });
175
+ }
176
+ return [...warnings, ...candidates];
177
+ }
32
178
  }
33
179
  async agentStats(signal) {
34
180
  signal.throwIfAborted();
35
181
  const agents = this.ctx.get('referenceLocalAgents');
36
182
  if (agents === undefined)
37
183
  return [];
38
- agents.setAgentDirectories(this.ctx.referenceChatHistory.getSettings().agentDirectories ?? {});
39
- return (await agents.stats(signal)).map(row => ({ agent: row.kind, conversations: row.conversations }));
184
+ const settings = this.ctx.referenceChatHistory.getSettings();
185
+ agents.setAgentDirectories(settings.agentDirectories ?? {});
186
+ const rows = [];
187
+ for (const kind of ALL_LOCAL_AGENTS) {
188
+ const format = scanFormat(kind);
189
+ signal.throwIfAborted();
190
+ const sessions = new Set();
191
+ const errors = [];
192
+ const custom = expandRoot(settings.agentDirectories?.[kind] ?? '', homedir());
193
+ for (const path of custom ? [undefined, custom] : [undefined]) {
194
+ try {
195
+ const result = discoverySchema.parse(await agents.scanner.scanDiscover({ format, ...(path ? { path } : {}) }, signal));
196
+ for (const row of result.sessions) {
197
+ if (await agents.conversationAvailability.visible({ format: row.format, path: row.sourcePath, sessionId: row.sessionId }))
198
+ sessions.add(JSON.stringify([row.sourcePath, row.sessionId]));
199
+ }
200
+ errors.push(...result.warnings.map(warning => `${warning.target}: ${warning.error}`));
201
+ }
202
+ catch (error) {
203
+ signal.throwIfAborted();
204
+ errors.push(error instanceof Error ? error.message : String(error));
205
+ }
206
+ }
207
+ rows.push({ agent: kind, conversations: sessions.size, ...(errors.length ? { error: errors.join('\n') } : {}) });
208
+ }
209
+ const roots = localConversationRoots();
210
+ return rows.map(row => {
211
+ const root = roots?.[scanFormat(row.agent)];
212
+ const custom = settings.agentDirectories?.[row.agent];
213
+ const paths = [...new Set([...(Array.isArray(root) ? root : root ? [root] : []), ...(custom ? [custom] : [])])];
214
+ return { ...row, ...(roots ? { paths } : {}), readable: true };
215
+ });
216
+ }
217
+ assertScannedSelection(agent, id) {
218
+ if (!id.startsWith('scan:') && !id.includes(':chat-import:'))
219
+ return;
220
+ const selected = this.scannedTargets.get(`${agent.session.header.id}:${id}`);
221
+ if (!selected || selected.owner !== agent.session.header.id || selected.policy !== JSON.stringify(this.ctx.referenceChatHistory.getSettings())) {
222
+ throw new Error('Conversation selection expired. Search and select it again.');
223
+ }
40
224
  }
41
225
  /**
42
226
  * Discovery for the `@` menu's cloud-drive group.
@@ -133,4 +317,8 @@ export class ReferenceAnythingRemote extends TypertRemoteService {
133
317
  async openListDisableMount(input, signal) { await this.ctx.openListManager.disableMount(input.id, input.disabled ?? true, signal); return true; }
134
318
  async openListRemoveMount(input, signal) { await this.ctx.openListManager.removeMount(input.id, signal); return true; }
135
319
  openListReindex(input, signal) { return this.ctx.openListManager.reindexMount(input.id, signal); }
320
+ localPlugins(signal) {
321
+ signal.throwIfAborted();
322
+ return resolveLocalPlugins(this.ctx);
323
+ }
136
324
  }
package/lib/index.d.ts CHANGED
@@ -76,6 +76,8 @@ export default class ReferenceRuntime extends Service {
76
76
  assertGranted(sessionId: string | undefined, ref: ReferenceRef): void;
77
77
  /** Allow a gated URI granted live or named in this task's durable user/tool-result history. */
78
78
  assertSessionGranted(session: Session | undefined, ref: ReferenceRef): void;
79
+ /** Resolve short handles only against references already named in this task. */
80
+ resolveReadReference(session: Session | undefined, uri: string): ReferenceRef;
79
81
  /** Release task-local grants when a host knows the task is gone. */
80
82
  revoke(sessionId: string): void;
81
83
  /**
@@ -90,7 +92,7 @@ export default class ReferenceRuntime extends Service {
90
92
  * @param signal - cancellation from the caller.
91
93
  * @returns matching items, capped at `limit`.
92
94
  */
93
- list(query?: string, limit?: number, signal?: AbortSignal): Promise<ReferenceSummary[]>;
95
+ list(query?: string, limit?: number, signal?: AbortSignal, externalOnly?: boolean): Promise<ReferenceSummary[]>;
94
96
  /**
95
97
  * Read one window of turns from a reference.
96
98
  * @param ref - the reference to resolve; its `source` selects the owning implementation.
package/lib/index.js CHANGED
@@ -12,7 +12,7 @@
12
12
  import { Context, Service } from '@deepseek-ai/cordis';
13
13
  import z from '@deepseek-ai/schemastery';
14
14
  import { ReferenceAnythingError } from "./errors.js";
15
- import { mayContainReference, parseReferenceText } from "./uri.js";
15
+ import { decodeReferenceUri, mayContainReference, parseReferenceText, referenceReadHandle } from "./uri.js";
16
16
  export { ReferenceAnythingError } from "./errors.js";
17
17
  export { REFERENCE_SCHEME, decodeReferenceUri, encodeReferenceUri, formatReferenceMention, mayContainReference, parseReferenceText, } from "./uri.js";
18
18
  /** Fan-out cap applied to discovery when a caller names no limit of its own. */
@@ -121,6 +121,34 @@ export default class ReferenceRuntime extends Service {
121
121
  }
122
122
  this.assertGranted(session && String(session.id), ref);
123
123
  }
124
+ /** Resolve short handles only against references already named in this task. */
125
+ resolveReadReference(session, uri) {
126
+ if (!uri.startsWith('ref:'))
127
+ return decodeReferenceUri(uri);
128
+ const candidates = [];
129
+ if (session) {
130
+ for (const key of this.grants.get(String(session.id)) ?? []) {
131
+ const separator = key.indexOf('\u0000');
132
+ candidates.push({ source: key.slice(0, separator), id: key.slice(separator + 1) });
133
+ }
134
+ for (const event of session.snapshotEvents()) {
135
+ const blocks = event.type === 'user/message' ? event.data.content
136
+ : event.type === 'tool/result' ? event.data.message.content : [];
137
+ for (const block of blocks) {
138
+ if (block.type !== 'text' || !mayContainReference(block.text))
139
+ continue;
140
+ try {
141
+ candidates.push(...parseReferenceText(block.text).references.map(input => input.ref));
142
+ }
143
+ catch { /* Ignore malformed history. */ }
144
+ }
145
+ }
146
+ }
147
+ const matched = candidates.find(ref => ref.source !== 'local-agent' && referenceReadHandle(ref) === uri);
148
+ if (!matched)
149
+ throw new ReferenceAnythingError('Unknown reference handle in this task; use external_reference_list and copy its uri exactly.', 'CONVERSATION_REFERENCE_NOT_GRANTED');
150
+ return matched;
151
+ }
124
152
  /** Release task-local grants when a host knows the task is gone. */
125
153
  revoke(sessionId) { this.grants.delete(sessionId); }
126
154
  /**
@@ -135,9 +163,9 @@ export default class ReferenceRuntime extends Service {
135
163
  * @param signal - cancellation from the caller.
136
164
  * @returns matching items, capped at `limit`.
137
165
  */
138
- async list(query = '', limit = this.listLimit, signal) {
166
+ async list(query = '', limit = this.listLimit, signal, externalOnly = false) {
139
167
  throwIfCancelled(signal);
140
- const available = await this.availableSources(signal);
168
+ const available = (await this.availableSources(signal)).filter(source => !externalOnly || source.id !== 'local-agent');
141
169
  if (available.length === 0)
142
170
  return [];
143
171
  const settled = await Promise.allSettled(available.map(source => source.list(query, limit, signal)));
@@ -0,0 +1,3 @@
1
+ import { defaultRoots, readConversation } from '../vendor/chat-import-reader/parsers.mjs';
2
+ export { defaultRoots as localConversationRoots };
3
+ export declare function readLocalConversation(input: Parameters<typeof readConversation>[1], signal?: AbortSignal): Promise<unknown>;
@@ -0,0 +1,81 @@
1
+ import { createReadStream } from 'node:fs';
2
+ import { readFile, readdir, stat } from 'node:fs/promises';
3
+ import { createHash } from 'node:crypto';
4
+ import { homedir } from 'node:os';
5
+ import { isAbsolute, join, resolve } from 'node:path';
6
+ import { defaultRoots, readConversation } from '../vendor/chat-import-reader/parsers.mjs';
7
+ export { defaultRoots as localConversationRoots };
8
+ // The parser gets only filesystem reads, never the Cordis host context.
9
+ export async function readLocalConversation(input, signal) {
10
+ if (!isAbsolute(input.path) || !input.sessionId)
11
+ throw new Error('Invalid conversation locator');
12
+ if (!Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 100)
13
+ throw new Error('Invalid conversation page size');
14
+ if (input.before !== undefined && (!Number.isSafeInteger(input.before) || input.before < 0))
15
+ throw new Error('Invalid conversation page offset');
16
+ if (input.format === 'claude' && /\.json$/i.test(input.path)) {
17
+ const raw = await readFile(input.path, { encoding: 'utf8', signal });
18
+ let metadata;
19
+ try {
20
+ metadata = JSON.parse(raw);
21
+ }
22
+ catch { /* Line-based transcripts are parsed below. */ }
23
+ if (metadata && typeof metadata === 'object' && 'cliSessionId' in metadata && !('messages' in metadata)) {
24
+ throw new Error('Conversation body unavailable: this Claude Desktop file contains metadata only. Open the conversation in Claude Desktop to restore its transcript, then refresh and reselect it. Do not retry or search unrelated files.');
25
+ }
26
+ }
27
+ const fingerprints = [];
28
+ const versions = async () => Promise.all([input.path, input.path + '-wal'].map(async (path) => {
29
+ try {
30
+ const info = await stat(path);
31
+ return `${info.size}:${info.mtimeMs}`;
32
+ }
33
+ catch (error) {
34
+ if (error.code === 'ENOENT')
35
+ return 'missing';
36
+ throw error;
37
+ }
38
+ }));
39
+ const initial = input.structured ? await versions() : [];
40
+ const target = (path) => ({ displayPath: resolve(path.startsWith('~/') || path.startsWith('~\\') ? join(homedir(), path.slice(2)) : path) });
41
+ const fs = {
42
+ streamText: async (value) => createReadStream(value.displayPath, { encoding: 'utf8', signal }),
43
+ resolve: async (path) => { signal?.throwIfAborted(); return target(path); },
44
+ processPath: (value) => value.displayPath,
45
+ stat: async (value) => {
46
+ signal?.throwIfAborted();
47
+ try {
48
+ const info = await stat(value.displayPath);
49
+ return { type: info.isDirectory() ? 'directory' : 'file', size: info.size, mtimeMs: info.mtimeMs, version: `${info.mtimeMs}:${info.size}` };
50
+ }
51
+ catch (error) {
52
+ if (error.code === 'ENOENT')
53
+ return undefined;
54
+ throw error;
55
+ }
56
+ },
57
+ readText: async (value) => {
58
+ signal?.throwIfAborted();
59
+ const text = await readFile(value.displayPath, { encoding: 'utf8', signal });
60
+ if (input.structured)
61
+ fingerprints.push(createHash('sha256').update(text).digest('hex'));
62
+ return text;
63
+ },
64
+ listDir: async (value) => {
65
+ signal?.throwIfAborted();
66
+ return (await readdir(value.displayPath, { withFileTypes: true })).map(entry => ({
67
+ name: entry.name, type: entry.isDirectory() ? 'directory' : 'file', target: target(join(value.displayPath, entry.name)),
68
+ }));
69
+ },
70
+ };
71
+ const result = await readConversation(fs, input.structured ? { ...input, revision: undefined } : input, signal);
72
+ if (input.structured && result && typeof result === 'object' && 'revision' in result) {
73
+ const final = await versions();
74
+ if (JSON.stringify(initial) !== JSON.stringify(final))
75
+ throw new Error('Conversation changed while reading; retry to refresh the preview');
76
+ result.revision = createHash('sha256').update(JSON.stringify([result.revision, fingerprints, final])).digest('hex');
77
+ if (input.revision && input.revision !== result.revision)
78
+ throw new Error('Conversation changed; read its latest page again');
79
+ }
80
+ return result;
81
+ }
@@ -0,0 +1,18 @@
1
+ import { discoverySchema } from './chat-import-discovery.ts';
2
+ type Input = {
3
+ format: string;
4
+ path?: string;
5
+ query?: string;
6
+ };
7
+ type Snapshot = ReturnType<typeof discoverySchema.parse>;
8
+ /** Keep SQLite and large export scans outside the DSH event loop. */
9
+ export declare class LocalConversationScanner {
10
+ private worker?;
11
+ private sequence;
12
+ private pending;
13
+ private cache;
14
+ scanDiscover(input: Input, signal?: AbortSignal): Promise<Snapshot>;
15
+ private scan;
16
+ dispose(error?: Error): void;
17
+ }
18
+ export {};
@@ -0,0 +1,55 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { AgentScanCache } from "./agent-scan-cache.js";
4
+ import { discoverySchema } from "./chat-import-discovery.js";
5
+ /** Keep SQLite and large export scans outside the DSH event loop. */
6
+ export class LocalConversationScanner {
7
+ worker;
8
+ sequence = 0;
9
+ pending = new Map();
10
+ cache = new AgentScanCache(30_000);
11
+ async scanDiscover(input, signal = new AbortController().signal) {
12
+ const result = await this.cache.read(this, '', JSON.stringify([input.format, input.path]), () => this.scan(input), signal);
13
+ const query = input.query?.trim().toLowerCase();
14
+ return query ? { ...result, sessions: result.sessions.filter(row => [row.title, row.project, row.sourcePath].some(s => s?.toLowerCase().includes(query))) } : result;
15
+ }
16
+ scan(input) {
17
+ if (!this.worker) {
18
+ const worker = this.worker = spawn(process.execPath, ['--max-old-space-size=512', fileURLToPath(new URL('../vendor/chat-import-reader/scan-worker.mjs', import.meta.url))], {
19
+ windowsHide: true, stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
20
+ });
21
+ worker.on('message', (message) => {
22
+ const request = this.pending.get(message.id);
23
+ if (!request)
24
+ return;
25
+ clearTimeout(request.timer);
26
+ this.pending.delete(message.id);
27
+ if (message.error)
28
+ request.reject(new Error(message.error));
29
+ else
30
+ request.resolve(message.result);
31
+ });
32
+ worker.on('error', error => this.dispose(error));
33
+ worker.on('exit', code => this.dispose(new Error(`Conversation scanner exited (${code})`)));
34
+ worker.unref();
35
+ worker.channel?.unref();
36
+ }
37
+ return new Promise((resolve, reject) => {
38
+ const id = ++this.sequence;
39
+ const timer = setTimeout(() => this.dispose(new Error('Conversation scan timed out')), 30_000);
40
+ this.pending.set(id, { resolve, reject, timer });
41
+ this.worker.send({ id, input }, error => { if (error)
42
+ this.dispose(error); });
43
+ });
44
+ }
45
+ dispose(error = new Error('Conversation scanner stopped')) {
46
+ this.worker?.removeAllListeners();
47
+ this.worker?.kill();
48
+ this.worker = undefined;
49
+ for (const request of this.pending.values()) {
50
+ clearTimeout(request.timer);
51
+ request.reject(error);
52
+ }
53
+ this.pending.clear();
54
+ }
55
+ }
@@ -0,0 +1,14 @@
1
+ /** Readable local locator shared by the composer and host. No Node imports. */
2
+ export interface LocalLocator {
3
+ source: string;
4
+ path: string;
5
+ sessionId: string;
6
+ }
7
+ export declare const LOCAL_REFERENCE_SCHEME = "dsh-local:";
8
+ export declare const LOCAL_REFERENCE_OPEN = "<local-agent-reference>";
9
+ export declare const LOCAL_REFERENCE_CLOSE = "</local-agent-reference>";
10
+ export declare function validateLocalLocator(value: unknown): LocalLocator;
11
+ export declare function localLocatorFromId(id: string): LocalLocator;
12
+ export declare function encodeLocalLocator(locator: LocalLocator): string;
13
+ export declare function decodeLocalLocator(uri: string): LocalLocator;
14
+ export declare function localLocatorId(locator: LocalLocator): string;
@@ -0,0 +1,34 @@
1
+ export const LOCAL_REFERENCE_SCHEME = 'dsh-local:';
2
+ export const LOCAL_REFERENCE_OPEN = '<local-agent-reference>';
3
+ export const LOCAL_REFERENCE_CLOSE = '</local-agent-reference>';
4
+ export function validateLocalLocator(value) {
5
+ if (!value || typeof value !== 'object')
6
+ throw new Error('Local reference must contain source, path and sessionId');
7
+ const { source, path, sessionId } = value;
8
+ if (![source, path, sessionId].every(v => typeof v === 'string' && v.length > 0 && v.length <= 16384 && !v.includes('\0')))
9
+ throw new Error('Local reference must contain source, path and sessionId');
10
+ return { source: source, path: path, sessionId: sessionId };
11
+ }
12
+ export function localLocatorFromId(id) {
13
+ const encoded = id.split(':chat-import:')[1];
14
+ if (!encoded)
15
+ throw new Error('Reselect this local conversation; the old reference protocol is no longer supported');
16
+ const base64 = encoded.replace(/-/g, '+').replace(/_/g, '/');
17
+ const parsed = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(base64), c => c.charCodeAt(0))));
18
+ return validateLocalLocator({ source: parsed.format, path: parsed.path, sessionId: parsed.sessionId });
19
+ }
20
+ export function encodeLocalLocator(locator) {
21
+ return LOCAL_REFERENCE_SCHEME + '?' + new URLSearchParams({ ...validateLocalLocator(locator) }).toString().replaceAll('(', '%28').replaceAll(')', '%29');
22
+ }
23
+ export function decodeLocalLocator(uri) {
24
+ if (!uri.startsWith(LOCAL_REFERENCE_SCHEME + '?'))
25
+ throw new Error('Invalid local conversation locator');
26
+ const fields = new URLSearchParams(uri.slice(LOCAL_REFERENCE_SCHEME.length + 1));
27
+ return validateLocalLocator({ source: fields.get('source'), path: fields.get('path'), sessionId: fields.get('sessionId') });
28
+ }
29
+ export function localLocatorId(locator) {
30
+ const kind = locator.source === 'claude' ? 'claude-code' : locator.source === 'gemini' ? 'gemini-cli' : locator.source;
31
+ const bytes = new TextEncoder().encode(JSON.stringify({ format: locator.source, path: locator.path, sessionId: locator.sessionId }));
32
+ const encoded = btoa(Array.from(bytes, byte => String.fromCharCode(byte)).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
33
+ return `${kind}:chat-import:${encoded}`;
34
+ }
@@ -0,0 +1,6 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { LocalPluginCandidate } from './contract.ts';
3
+ import type { ReferenceSource } from './types.ts';
4
+ export declare function createPluginReferenceSource(ctx: Context): ReferenceSource;
5
+ /** Read the active loader's composed entries, including bundle patches and effective disables. */
6
+ export declare function resolveLocalPlugins(ctx: Context): Promise<readonly LocalPluginCandidate[]>;