dsh-reference-anything 0.3.0 → 0.3.2

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 (109) hide show
  1. package/LICENSE +22 -22
  2. package/NOTICE.md +52 -25
  3. package/README.md +339 -286
  4. package/cordis.patch.yml +89 -18
  5. package/lib/client.js +13538 -591
  6. package/lib/contract.d.ts +155 -16
  7. package/lib/contract.js +68 -7
  8. package/lib/download-directory.d.ts +18 -0
  9. package/lib/download-directory.js +55 -0
  10. package/lib/errors.d.ts +4 -0
  11. package/lib/host.d.ts +96 -8
  12. package/lib/host.js +88 -19
  13. package/lib/index.d.ts +22 -2
  14. package/lib/index.js +57 -7
  15. package/lib/opencli.d.ts +5 -3
  16. package/lib/opencli.js +13 -6
  17. package/lib/openlist/host.d.ts +157 -0
  18. package/lib/openlist/host.js +873 -0
  19. package/lib/openlist/index.d.ts +84 -0
  20. package/lib/openlist/index.js +507 -0
  21. package/lib/render.d.ts +12 -7
  22. package/lib/render.js +27 -21
  23. package/lib/sources/cloud-drive/cache.d.ts +60 -0
  24. package/lib/sources/cloud-drive/cache.js +107 -0
  25. package/lib/sources/cloud-drive/index.d.ts +190 -0
  26. package/lib/sources/cloud-drive/index.js +526 -0
  27. package/lib/sources/cloud-drive/providers/http.d.ts +27 -0
  28. package/lib/sources/cloud-drive/providers/http.js +73 -0
  29. package/lib/sources/cloud-drive/providers/openlist.d.ts +59 -0
  30. package/lib/sources/cloud-drive/providers/openlist.js +535 -0
  31. package/lib/sources/cloud-drive/registry.d.ts +53 -0
  32. package/lib/sources/cloud-drive/registry.js +101 -0
  33. package/lib/sources/cloud-drive/types.d.ts +169 -0
  34. package/lib/sources/cloud-drive/types.js +17 -0
  35. package/lib/sources/file.d.ts +2 -0
  36. package/lib/sources/file.js +2 -0
  37. package/lib/sources/local-agent/adapters/claude-code.d.ts +16 -0
  38. package/lib/sources/local-agent/adapters/claude-code.js +171 -0
  39. package/lib/sources/local-agent/adapters/codex.d.ts +20 -0
  40. package/lib/sources/local-agent/adapters/codex.js +204 -0
  41. package/lib/sources/local-agent/adapters/cursor.d.ts +23 -0
  42. package/lib/sources/local-agent/adapters/cursor.js +121 -0
  43. package/lib/sources/local-agent/adapters/gemini-cli.d.ts +20 -0
  44. package/lib/sources/local-agent/adapters/gemini-cli.js +141 -0
  45. package/lib/sources/local-agent/adapters/grokbuild.d.ts +22 -0
  46. package/lib/sources/local-agent/adapters/grokbuild.js +76 -0
  47. package/lib/sources/local-agent/adapters/hermes.d.ts +19 -0
  48. package/lib/sources/local-agent/adapters/hermes.js +85 -0
  49. package/lib/sources/local-agent/adapters/kimi.d.ts +28 -0
  50. package/lib/sources/local-agent/adapters/kimi.js +261 -0
  51. package/lib/sources/local-agent/adapters/openclaw.d.ts +21 -0
  52. package/lib/sources/local-agent/adapters/openclaw.js +120 -0
  53. package/lib/sources/local-agent/adapters/opencode.d.ts +44 -0
  54. package/lib/sources/local-agent/adapters/opencode.js +236 -0
  55. package/lib/sources/local-agent/adapters/pi.d.ts +26 -0
  56. package/lib/sources/local-agent/adapters/pi.js +242 -0
  57. package/lib/sources/local-agent/adapters/qoder.d.ts +19 -0
  58. package/lib/sources/local-agent/adapters/qoder.js +93 -0
  59. package/lib/sources/local-agent/adapters/reasonix.d.ts +23 -0
  60. package/lib/sources/local-agent/adapters/reasonix.js +90 -0
  61. package/lib/sources/local-agent/adapters/shared.d.ts +163 -0
  62. package/lib/sources/local-agent/adapters/shared.js +415 -0
  63. package/lib/sources/local-agent/adapters/sqlite-shared.d.ts +92 -0
  64. package/lib/sources/local-agent/adapters/sqlite-shared.js +215 -0
  65. package/lib/sources/local-agent/adapters/zcode.d.ts +41 -0
  66. package/lib/sources/local-agent/adapters/zcode.js +159 -0
  67. package/lib/sources/local-agent/index.d.ts +222 -0
  68. package/lib/sources/local-agent/index.js +656 -0
  69. package/lib/sources/local-agent/page.d.ts +55 -0
  70. package/lib/sources/local-agent/page.js +83 -0
  71. package/lib/sources/local-agent/path.d.ts +8 -0
  72. package/lib/sources/local-agent/path.js +43 -0
  73. package/lib/sources/local-agent/registry.d.ts +68 -0
  74. package/lib/sources/local-agent/registry.js +116 -0
  75. package/lib/sources/local-agent/scan.d.ts +175 -0
  76. package/lib/sources/local-agent/scan.js +472 -0
  77. package/lib/sources/local-agent/sqlite.d.ts +45 -0
  78. package/lib/sources/local-agent/sqlite.js +113 -0
  79. package/lib/sources/local-agent/store.d.ts +93 -0
  80. package/lib/sources/local-agent/store.js +113 -0
  81. package/lib/sources/local-agent/types.d.ts +338 -0
  82. package/lib/sources/local-agent/types.js +20 -0
  83. package/lib/sources/web-chat/index.d.ts +5 -1
  84. package/lib/sources/web-chat/index.js +47 -8
  85. package/lib/store/spec.d.ts +50 -4
  86. package/lib/store/spec.js +2 -2
  87. package/lib/sync/index.d.ts +1 -0
  88. package/lib/sync/index.js +28 -8
  89. package/lib/tool.d.ts +5 -0
  90. package/lib/tool.js +418 -45
  91. package/lib/typert.js +13 -2
  92. package/lib/types.d.ts +28 -2
  93. package/lib/update.d.ts +6 -0
  94. package/lib/update.js +29 -1
  95. package/lib/web.js +5 -1
  96. package/lib/wire.d.ts +80 -9
  97. package/lib/wire.js +30 -4
  98. package/opencli-plugin/chatgpt.js +30 -27
  99. package/opencli-plugin/claude.js +12 -8
  100. package/opencli-plugin/common.js +266 -187
  101. package/opencli-plugin/deepseek.js +8 -5
  102. package/opencli-plugin/gemini.js +42 -38
  103. package/opencli-plugin/grok.js +22 -21
  104. package/opencli-plugin/kimi.js +34 -31
  105. package/opencli-plugin/opencli-plugin.json +6 -6
  106. package/opencli-plugin/package.json +8 -8
  107. package/package.json +182 -167
  108. package/lib/profile-mode.d.ts +0 -30
  109. package/lib/profile-mode.js +0 -90
package/lib/host.js CHANGED
@@ -1,7 +1,65 @@
1
1
  import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
2
- import { switchReferenceUiMode as switchProfileReferenceUiMode } from "./profile-mode.js";
2
+ import { tmpdir } from 'node:os';
3
+ import { spawn } from 'node:child_process';
4
+ import { validateDownloadDirectory } from "./download-directory.js";
3
5
  export class ReferenceAnythingRemote extends TypertRemoteService {
4
6
  constructor(ctx) { super(ctx, 'referenceAnything'); }
7
+ /**
8
+ * Discovery for the `@` menu's local-agent group.
9
+ *
10
+ * Resolved optionally rather than injected: `reference-anything-web` must
11
+ * still load in a profile where the local-agent plugin is switched off, and an
12
+ * absent service means an empty group, not a broken composer. Workspace
13
+ * scoping takes the session's own cwd, so a Web server serving projects from
14
+ * anywhere still scopes to the project the user is in.
15
+ */
16
+ async agentSearch(agent, input, signal) {
17
+ const agents = this.ctx.get('referenceLocalAgents');
18
+ if (agents === undefined)
19
+ 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
+ }));
32
+ }
33
+ async agentStats(signal) {
34
+ signal.throwIfAborted();
35
+ const agents = this.ctx.get('referenceLocalAgents');
36
+ if (agents === undefined)
37
+ return [];
38
+ agents.setAgentDirectories(this.ctx.referenceChatHistory.getSettings().agentDirectories ?? {});
39
+ return (await agents.stats(signal)).map(row => ({ agent: row.kind, conversations: row.conversations }));
40
+ }
41
+ /**
42
+ * Discovery for the `@` menu's cloud-drive group.
43
+ *
44
+ * Optional for the same reason as `agentSearch` above. It takes no session:
45
+ * a drive is not scoped to a workspace, so there is nothing a cwd could
46
+ * narrow. `list` already refuses to name a download URL, so what comes back
47
+ * here is a label and a display path.
48
+ */
49
+ async driveSearch(input, signal) {
50
+ const drives = this.ctx.get('referenceCloudDrive');
51
+ if (drives === undefined)
52
+ return [];
53
+ const rows = await drives.pickerList(input.query, input.limit, signal);
54
+ return rows.map(row => ({
55
+ id: row.ref.id,
56
+ label: row.label,
57
+ provider: row.provider ?? '',
58
+ ...row.origin === undefined ? {} : { origin: row.origin },
59
+ ...row.updatedAt === undefined ? {} : { updatedAt: row.updatedAt },
60
+ ...row.isDirectory === true ? { isDirectory: true } : {},
61
+ }));
62
+ }
5
63
  search(input, signal) {
6
64
  signal.throwIfAborted();
7
65
  return this.ctx.referenceChatHistory.search(input.query, input.provider, input.limit, signal);
@@ -16,19 +74,6 @@ export class ReferenceAnythingRemote extends TypertRemoteService {
16
74
  updateStatus(signal) { return this.ctx.referenceChatHistory.updateStatus(signal); }
17
75
  checkUpdate(signal) { return this.ctx.referenceChatHistory.checkUpdate(signal); }
18
76
  installUpdate(signal) { return this.ctx.referenceChatHistory.installUpdate(signal); }
19
- async switchReferenceUiMode(input, signal) {
20
- const settings = this.ctx.referenceChatHistory.getSettings();
21
- const previousMode = settings.referenceUiMode ?? 'plugin';
22
- const result = await switchProfileReferenceUiMode({ mode: input.mode, signal });
23
- try {
24
- await this.ctx.referenceChatHistory.updateSettings({ ...settings, referenceUiMode: input.mode });
25
- }
26
- catch (error) {
27
- await switchProfileReferenceUiMode({ mode: previousMode }).catch(() => undefined);
28
- throw error;
29
- }
30
- return result;
31
- }
32
77
  stats() { return this.ctx.referenceChatHistory.stats(); }
33
78
  syncStart(input) {
34
79
  return this.ctx.referenceChatHistory.sync.start(input.providers, input.mode);
@@ -36,7 +81,24 @@ export class ReferenceAnythingRemote extends TypertRemoteService {
36
81
  syncStatus(input) { return this.ctx.referenceChatHistory.sync.status(input.jobId); }
37
82
  syncCancel(input) { return this.ctx.referenceChatHistory.sync.cancel(input.jobId); }
38
83
  settingsGet() { return this.ctx.referenceChatHistory.getSettings(); }
39
- settingsUpdate(settings) { return this.ctx.referenceChatHistory.updateSettings(settings); }
84
+ async settingsUpdate(settings) {
85
+ const value = await this.ctx.referenceChatHistory.updateSettings(settings);
86
+ this.ctx.get('referenceLocalAgents')?.setAgentDirectories(value.agentDirectories ?? {});
87
+ return value;
88
+ }
89
+ async openDownloadDirectory(signal) {
90
+ signal.throwIfAborted();
91
+ const configured = this.ctx.referenceChatHistory.getSettings().cloudDriveDownloadDirectory;
92
+ const path = configured || tmpdir();
93
+ await validateDownloadDirectory(path);
94
+ const child = process.platform === 'win32'
95
+ ? spawn('explorer.exe', [path], { detached: true, stdio: 'ignore' })
96
+ : process.platform === 'darwin'
97
+ ? spawn('open', [path], { detached: true, stdio: 'ignore' })
98
+ : spawn('xdg-open', [path], { detached: true, stdio: 'ignore' });
99
+ child.unref();
100
+ return true;
101
+ }
40
102
  browse(input, signal) {
41
103
  signal.throwIfAborted();
42
104
  return this.ctx.referenceChatHistory.browse(input.query, input.provider, input.limit, input.offset);
@@ -50,10 +112,6 @@ export class ReferenceAnythingRemote extends TypertRemoteService {
50
112
  signal.throwIfAborted();
51
113
  return this.ctx.referenceChatHistory.removeProvider(input.provider);
52
114
  }
53
- clearOlder(input, signal) {
54
- signal.throwIfAborted();
55
- return this.ctx.referenceChatHistory.removeOlderThan(input.days);
56
- }
57
115
  clearRemoteMissing(signal) {
58
116
  signal.throwIfAborted();
59
117
  return this.ctx.referenceChatHistory.removeRemoteMissing();
@@ -63,4 +121,15 @@ export class ReferenceAnythingRemote extends TypertRemoteService {
63
121
  return this.ctx.referenceChatHistory.removeOldAccounts();
64
122
  }
65
123
  syncStates() { return this.ctx.referenceChatHistory.syncStates(); }
124
+ openListStatus(signal) { return this.ctx.openListManager.status(signal); }
125
+ openListInstall(signal) { return this.ctx.openListManager.install(signal); }
126
+ openListUpgrade(input, signal) { return this.ctx.openListManager.upgrade(input, signal); }
127
+ openListConnectExternal(input, signal) { return this.ctx.openListManager.connectExternal(input, signal); }
128
+ openListDisconnect(signal) { return this.ctx.openListManager.disconnect(signal); }
129
+ openListDrivers(signal) { return this.ctx.openListManager.drivers(signal); }
130
+ openListMounts(signal) { return this.ctx.openListManager.mounts(signal); }
131
+ openListCreateMount(input, signal) { return this.ctx.openListManager.createMount(input, signal); }
132
+ async openListDisableMount(input, signal) { await this.ctx.openListManager.disableMount(input.id, input.disabled ?? true, signal); return true; }
133
+ async openListRemoveMount(input, signal) { await this.ctx.openListManager.removeMount(input.id, signal); return true; }
134
+ openListReindex(input, signal) { return this.ctx.openListManager.reindexMount(input.id, signal); }
66
135
  }
package/lib/index.d.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  * @module dsh-reference-anything
11
11
  */
12
12
  import { Context, Service } from '@deepseek-ai/cordis';
13
+ import type { Session } from '@deepseek-ai/dsh-session';
13
14
  import z from '@deepseek-ai/schemastery';
14
15
  import type { ReferenceRef, ReferenceSnapshot, ReferenceSource, ReferenceSummary, ReferenceWindow } from './types.ts';
15
16
  export type * from './types.ts';
@@ -52,10 +53,29 @@ export default class ReferenceRuntime extends Service {
52
53
  registerSource(source: ReferenceSource): () => void;
53
54
  /** @returns the registered source ids, in registration order. */
54
55
  sourceIds(): string[];
55
- /** Authorize one task to read a Web conversation named by its user or discovery tool. */
56
+ /**
57
+ * Whether a source's items may only be read after an explicit grant.
58
+ *
59
+ * An unregistered source fails closed: registration order must never decide
60
+ * authorization, and `read()` would reject it as `SOURCE_UNKNOWN` a moment
61
+ * later anyway.
62
+ */
63
+ private gated;
64
+ /**
65
+ * Qualify the grant by source.
66
+ *
67
+ * Two sources can mint the same opaque id, and an unqualified key would let
68
+ * a grant for one silently authorize the other. NUL cannot occur in either
69
+ * half, so the join is unambiguous.
70
+ */
71
+ private static grantKey;
72
+ private static contentMentions;
73
+ /** Authorize one task to read a gated reference named by its user or discovery tool. */
56
74
  grant(sessionId: string, ref: ReferenceRef): void;
57
- /** Enforce that a Web conversation URI originated in this task's mention/list surface. */
75
+ /** Enforce that a gated URI originated in this task's mention/list surface. */
58
76
  assertGranted(sessionId: string | undefined, ref: ReferenceRef): void;
77
+ /** Allow a gated URI granted live or named in this task's durable user/tool-result history. */
78
+ assertSessionGranted(session: Session | undefined, ref: ReferenceRef): void;
59
79
  /** Release task-local grants when a host knows the task is gone. */
60
80
  revoke(sessionId: string): void;
61
81
  /**
package/lib/index.js CHANGED
@@ -12,6 +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
16
  export { ReferenceAnythingError } from "./errors.js";
16
17
  export { REFERENCE_SCHEME, decodeReferenceUri, encodeReferenceUri, formatReferenceMention, mayContainReference, parseReferenceText, } from "./uri.js";
17
18
  /** Fan-out cap applied to discovery when a caller names no limit of its own. */
@@ -57,22 +58,71 @@ export default class ReferenceRuntime extends Service {
57
58
  sourceIds() {
58
59
  return [...this.sources.keys()];
59
60
  }
60
- /** Authorize one task to read a Web conversation named by its user or discovery tool. */
61
+ /**
62
+ * Whether a source's items may only be read after an explicit grant.
63
+ *
64
+ * An unregistered source fails closed: registration order must never decide
65
+ * authorization, and `read()` would reject it as `SOURCE_UNKNOWN` a moment
66
+ * later anyway.
67
+ */
68
+ gated(source) {
69
+ const registered = this.sources.get(source);
70
+ return registered === undefined || registered.requiresGrant === true;
71
+ }
72
+ /**
73
+ * Qualify the grant by source.
74
+ *
75
+ * Two sources can mint the same opaque id, and an unqualified key would let
76
+ * a grant for one silently authorize the other. NUL cannot occur in either
77
+ * half, so the join is unambiguous.
78
+ */
79
+ static grantKey(ref) { return `${ref.source}\u0000${ref.id}`; }
80
+ static contentMentions(blocks, key) {
81
+ return blocks.some(block => {
82
+ if (block.type === 'tool-result')
83
+ return ReferenceRuntime.contentMentions(block.content, key);
84
+ if (block.type !== 'text' || !mayContainReference(block.text))
85
+ return false;
86
+ try {
87
+ return parseReferenceText(block.text).references.some(input => ReferenceRuntime.grantKey(input.ref) === key);
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ });
93
+ }
94
+ /** Authorize one task to read a gated reference named by its user or discovery tool. */
61
95
  grant(sessionId, ref) {
62
- if (ref.source !== 'web-chat')
63
- return;
96
+ // Recorded unconditionally: a source may register after the mention that
97
+ // named it, and a grant for an ungated source is merely inert.
64
98
  const values = this.grants.get(sessionId) ?? new Set();
65
- values.add(ref.id);
99
+ values.add(ReferenceRuntime.grantKey(ref));
66
100
  this.grants.set(sessionId, values);
67
101
  }
68
- /** Enforce that a Web conversation URI originated in this task's mention/list surface. */
102
+ /** Enforce that a gated URI originated in this task's mention/list surface. */
69
103
  assertGranted(sessionId, ref) {
70
- if (ref.source !== 'web-chat')
104
+ if (!this.gated(ref.source))
71
105
  return;
72
- if (!sessionId || !this.grants.get(sessionId)?.has(ref.id)) {
106
+ if (!sessionId || !this.grants.get(sessionId)?.has(ReferenceRuntime.grantKey(ref))) {
73
107
  throw new ReferenceAnythingError('this task has not been granted access to that conversation reference', 'CONVERSATION_REFERENCE_NOT_GRANTED');
74
108
  }
75
109
  }
110
+ /** Allow a gated URI granted live or named in this task's durable user/tool-result history. */
111
+ assertSessionGranted(session, ref) {
112
+ const key = ReferenceRuntime.grantKey(ref);
113
+ if (session && !this.grants.get(String(session.id))?.has(key)) {
114
+ const mentioned = session.events.some(event => {
115
+ if (event.type === 'user/message')
116
+ return ReferenceRuntime.contentMentions(event.data.content, key);
117
+ if (event.type === 'tool/result')
118
+ return ReferenceRuntime.contentMentions(event.data.message.content, key);
119
+ return false;
120
+ });
121
+ if (mentioned)
122
+ this.grant(String(session.id), ref);
123
+ }
124
+ this.assertGranted(session && String(session.id), ref);
125
+ }
76
126
  /** Release task-local grants when a host knows the task is gone. */
77
127
  revoke(sessionId) { this.grants.delete(sessionId); }
78
128
  /**
package/lib/opencli.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import type { ChatProvider } from './store/spec.ts';
2
2
  import type { ProviderConversationRow, ProviderTurnRow } from './store/store.ts';
3
3
  export declare const MIN_OPENCLI_VERSION = "1.8.6";
4
- export declare const MIN_ADAPTER_VERSION = "0.2.2";
4
+ export declare const MIN_ADAPTER_VERSION = "0.2.3";
5
+ /** Allows a 25 MiB attachment to cross JSON/base64 stdout with bounded headroom. */
6
+ export declare const DEFAULT_OPENCLI_MAX_STDOUT_BYTES: number;
5
7
  export declare const OPENCLI_NPM_PACKAGE = "@jackwener/opencli";
6
- export type OpenCliErrorCode = 'EXTENSION_NOT_CONNECTED' | 'PROVIDER_TIMEOUT' | 'PROVIDER_NOT_LOGGED_IN' | 'PROVIDER_ACCOUNT_MISMATCH' | 'OPENCLI_CONFIGURATION' | 'OPENCLI_OUTPUT_TOO_LARGE' | 'OPENCLI_FAILED' | 'OPENCLI_INSTALL_FAILED';
8
+ export type OpenCliErrorCode = 'EXTENSION_NOT_CONNECTED' | 'PROVIDER_TIMEOUT' | 'PROVIDER_NOT_LOGGED_IN' | 'PROVIDER_ACCOUNT_MISMATCH' | 'PROVIDER_RATE_LIMIT' | 'OPENCLI_CONFIGURATION' | 'OPENCLI_OUTPUT_TOO_LARGE' | 'OPENCLI_FAILED' | 'OPENCLI_INSTALL_FAILED' | 'ATTACHMENT_TOO_LARGE';
7
9
  export declare class OpenCliError extends Error {
8
10
  readonly code: OpenCliErrorCode;
9
11
  constructor(message: string, code: OpenCliErrorCode, options?: ErrorOptions);
@@ -95,7 +97,7 @@ export declare class OpenCliRunner {
95
97
  /** Resolve account identity and history through one OpenCLI browser command. */
96
98
  syncIndex(provider: ChatProvider, signal?: AbortSignal, since?: string, accountScope?: string): Promise<ProviderIndex>;
97
99
  detail(provider: ChatProvider, id: string, signal?: AbortSignal, accountScope?: string): Promise<ProviderTurnRow[]>;
98
- attachment(provider: ChatProvider, locator: string, output: string, maxBytes: number, signal?: AbortSignal): Promise<Record<string, unknown>>;
100
+ attachment(provider: ChatProvider, locator: string, output: string, maxBytes: number, signal?: AbortSignal, accountScope?: string): Promise<Record<string, unknown>>;
99
101
  health(signal?: AbortSignal): Promise<OpenCliHealth>;
100
102
  /** Inspect local OpenCLI, daemon, extension and plugin state without an active connectivity probe. */
101
103
  quickHealth(signal?: AbortSignal): Promise<OpenCliHealth>;
package/lib/opencli.js CHANGED
@@ -5,7 +5,9 @@ import { promisify } from 'node:util';
5
5
  import { createHash } from 'node:crypto';
6
6
  const execFile = promisify(nodeExecFile);
7
7
  export const MIN_OPENCLI_VERSION = '1.8.6';
8
- export const MIN_ADAPTER_VERSION = '0.2.2';
8
+ export const MIN_ADAPTER_VERSION = '0.2.3';
9
+ /** Allows a 25 MiB attachment to cross JSON/base64 stdout with bounded headroom. */
10
+ export const DEFAULT_OPENCLI_MAX_STDOUT_BYTES = 40 * 1024 * 1024;
9
11
  export const OPENCLI_NPM_PACKAGE = '@jackwener/opencli';
10
12
  const SITE = {
11
13
  chatgpt: 'dsh-chatgpt', claude: 'dsh-claude', gemini: 'dsh-gemini',
@@ -55,7 +57,7 @@ export class OpenCliRunner {
55
57
  this.profile = options.profile || '';
56
58
  this.prefixArgs = options.prefixArgs ?? [];
57
59
  this.timeoutMs = options.timeoutMs ?? 60_000;
58
- this.maxStdoutBytes = options.maxStdoutBytes ?? 32 * 1024 * 1024;
60
+ this.maxStdoutBytes = options.maxStdoutBytes ?? DEFAULT_OPENCLI_MAX_STDOUT_BYTES;
59
61
  }
60
62
  async whoami(provider, signal) {
61
63
  const rows = await this.json(SITE[provider], 'whoami', [], signal);
@@ -107,8 +109,11 @@ export class OpenCliRunner {
107
109
  };
108
110
  });
109
111
  }
110
- async attachment(provider, locator, output, maxBytes, signal) {
111
- const rows = await this.json(SITE[provider], 'attachment', [locator, '--output', output, '--maxBytes', String(maxBytes)], signal);
112
+ async attachment(provider, locator, output, maxBytes, signal, accountScope = '') {
113
+ const rows = await this.json(SITE[provider], 'attachment', [
114
+ locator, '--output', output, '--maxBytes', String(maxBytes),
115
+ ...(accountScope ? ['--accountScope', accountScope] : []),
116
+ ], signal);
112
117
  return rows[0] ?? {};
113
118
  }
114
119
  async health(signal) {
@@ -246,8 +251,10 @@ export class OpenCliRunner {
246
251
  const stderr = String(detail.stderr || detail.stdout || '').trim().slice(0, 2_000);
247
252
  const exit = typeof detail.code === 'number' ? detail.code : undefined;
248
253
  const code = stderr.includes('DSH_ACCOUNT_SCOPE_MISMATCH') ? 'PROVIDER_ACCOUNT_MISMATCH'
249
- : exit === 69 ? 'EXTENSION_NOT_CONNECTED' : exit === 75 ? 'PROVIDER_TIMEOUT'
250
- : exit === 77 ? 'PROVIDER_NOT_LOGGED_IN' : exit === 78 ? 'OPENCLI_CONFIGURATION' : 'OPENCLI_FAILED';
254
+ : stderr.includes('DSH_PROVIDER_RATE_LIMIT') ? 'PROVIDER_RATE_LIMIT'
255
+ : stderr.includes('DSH_ATTACHMENT_TOO_LARGE') ? 'ATTACHMENT_TOO_LARGE'
256
+ : exit === 69 ? 'EXTENSION_NOT_CONNECTED' : exit === 75 ? 'PROVIDER_TIMEOUT'
257
+ : exit === 77 ? 'PROVIDER_NOT_LOGGED_IN' : exit === 78 ? 'OPENCLI_CONFIGURATION' : 'OPENCLI_FAILED';
251
258
  throw new OpenCliError(stderr || `OpenCLI exited with ${String(detail.code)}`, code, { cause: error });
252
259
  }
253
260
  }
@@ -0,0 +1,157 @@
1
+ import { chmod, copyFile, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
2
+ import { spawn as nodeSpawn } from 'node:child_process';
3
+ /** Deliberately fixed; upgrades are a conscious manifest change. */
4
+ export declare const OPENLIST_FIXED_VERSION = "v4.2.2";
5
+ export declare const OPENLIST_CREDENTIAL_DIRECTORY: string;
6
+ export declare const OPENLIST_CREDENTIAL_FILE: string;
7
+ export interface OpenListAsset {
8
+ readonly platform: string;
9
+ readonly arch: string;
10
+ readonly url: string;
11
+ readonly sha256: string;
12
+ readonly archive: 'tar.gz' | 'zip';
13
+ readonly binary: string;
14
+ }
15
+ export interface OpenListReleaseManifest {
16
+ readonly version: string;
17
+ readonly assets: readonly OpenListAsset[];
18
+ }
19
+ /** SHA-256 values were computed from the six named GitHub v4.2.2 release assets. */
20
+ export declare const OPENLIST_RELEASE: OpenListReleaseManifest;
21
+ export interface OpenListCredentials {
22
+ readonly endpoint: string;
23
+ readonly username?: string;
24
+ readonly password?: string;
25
+ readonly token?: string;
26
+ readonly mode?: 'managed' | 'external';
27
+ readonly version?: string; /** Host-only cache revision. */
28
+ readonly generation?: number;
29
+ }
30
+ export type OpenListAdminMutationState = 'not-started' | 'committed' | 'unknown';
31
+ /** Safe administration facts. No storage addition, credentials, or raw URLs cross this boundary. */
32
+ export interface OpenListMount {
33
+ readonly id: string;
34
+ readonly name: string;
35
+ readonly driver: string;
36
+ readonly enabled: boolean;
37
+ readonly status?: 'ready' | 'disabled' | 'error';
38
+ readonly error?: string;
39
+ readonly capacityUsed?: number;
40
+ readonly capacityTotal?: number;
41
+ readonly indexStatus?: 'idle' | 'running' | 'complete' | 'failed';
42
+ readonly indexProgress?: number;
43
+ readonly indexCount?: number;
44
+ }
45
+ export interface OpenListDriverField {
46
+ readonly name: string;
47
+ readonly label: string;
48
+ readonly type: 'text' | 'password' | 'number' | 'boolean' | 'select';
49
+ readonly secret: boolean;
50
+ readonly required: boolean;
51
+ readonly hasDefault?: boolean;
52
+ readonly default?: string | number | boolean;
53
+ readonly options?: readonly {
54
+ readonly label: string;
55
+ readonly value: string;
56
+ }[];
57
+ }
58
+ export interface OpenListDriver {
59
+ readonly name: string;
60
+ readonly description?: string;
61
+ readonly quickAuth: boolean;
62
+ readonly fields: readonly OpenListDriverField[];
63
+ }
64
+ /** Host-only mount creation values. `addition` is serialized only on the Host. */
65
+ export interface OpenListMountInput {
66
+ readonly mountPath: string;
67
+ readonly driver: string;
68
+ readonly addition: Record<string, unknown>;
69
+ readonly order?: number;
70
+ readonly remark?: string;
71
+ }
72
+ export interface OpenListSeams {
73
+ readonly fetch?: typeof fetch;
74
+ readonly spawn?: typeof nodeSpawn;
75
+ readonly readFile?: typeof readFile;
76
+ readonly writeFile?: typeof writeFile;
77
+ readonly mkdir?: typeof mkdir;
78
+ readonly chmod?: typeof chmod;
79
+ readonly rename?: typeof rename;
80
+ readonly copyFile?: typeof copyFile;
81
+ readonly unlink?: typeof unlink;
82
+ readonly fsyncFile?: (path: string) => Promise<void>;
83
+ /** Required on Windows to apply a restrictive ACL (for example via icacls). */
84
+ readonly securePermissions?: (path: string, directory: boolean) => Promise<void>;
85
+ readonly sleep?: (ms: number) => Promise<void>;
86
+ /** Test/integration seam for the executable replacement transaction. */
87
+ readonly install?: typeof installOpenList;
88
+ }
89
+ /** Exchange external username/password for the raw OpenList token. */
90
+ export declare function authenticateOpenList(endpoint: string, username: string, password: string, fetchImpl?: (input: string, init?: RequestInit) => Promise<Response>, signal?: AbortSignal): Promise<string>;
91
+ /** Download only a manifest-pinned binary after validating its SHA-256. */
92
+ export declare function installOpenList(dataRoot: string, manifest?: OpenListReleaseManifest, seams?: OpenListSeams, os?: NodeJS.Platform, cpu?: string, signal?: AbortSignal, retainBackup?: boolean): Promise<{
93
+ version: string;
94
+ path: string;
95
+ finish: (success: boolean) => Promise<void>;
96
+ }>;
97
+ /** Extract only the expected executable; archive paths never reach the filesystem. */
98
+ export declare function unpackOpenListBinary(archive: Uint8Array, asset: OpenListAsset): Uint8Array;
99
+ /** Select exactly the release asset for this OS and CPU architecture. */
100
+ export declare function selectOpenListAsset(manifest: OpenListReleaseManifest, os?: NodeJS.Platform, cpu?: string): OpenListAsset;
101
+ export declare function sha256(bytes: Uint8Array): string;
102
+ export declare function verifyOpenListAsset(bytes: Uint8Array, expected: string): boolean;
103
+ /** Validate an endpoint without leaking its full value in a diagnostic. */
104
+ export declare function validateOpenListEndpoint(value: string, managed?: boolean): string;
105
+ export declare function isLoopback(host: string): boolean;
106
+ /** Ask the kernel for a loopback port. Start immediately afterwards to minimize the normal port race. */
107
+ export declare function chooseOpenListPort(): Promise<number>;
108
+ export declare function readOpenListCredentials(path?: string, seams?: OpenListSeams): Promise<OpenListCredentials | undefined>;
109
+ export declare function writeOpenListCredentials(credentials: OpenListCredentials, path?: string, seams?: OpenListSeams, signal?: AbortSignal): Promise<void>;
110
+ /** A Host-private authenticated API wrapper with redacted errors and safe mount shapes. */
111
+ export declare class OpenListHostClient {
112
+ private credentials;
113
+ private readonly refreshToken?;
114
+ private readonly managed;
115
+ private readonly fetchImpl;
116
+ private readonly secretDefaults;
117
+ constructor(credentials: OpenListCredentials, seams?: OpenListSeams, refreshToken?: ((signal?: AbortSignal) => Promise<OpenListCredentials>) | undefined, managed?: boolean);
118
+ connect(signal?: AbortSignal): Promise<void>;
119
+ drivers(signal?: AbortSignal): Promise<string[]>;
120
+ driverInfo(signal?: AbortSignal): Promise<OpenListDriver[]>;
121
+ fillSecretDefaults(driver: string, supplied: Record<string, unknown>): Record<string, unknown>;
122
+ mounts(signal?: AbortSignal): Promise<OpenListMount[]>;
123
+ createMount(input: OpenListMountInput, signal?: AbortSignal): Promise<OpenListMount>;
124
+ updateMount(id: string, input: OpenListMountInput, signal?: AbortSignal): Promise<OpenListMount>;
125
+ /** Reauthentication patches only credential fields onto the full host-side row. */
126
+ updateMountPatch(id: string, additionPatch: Record<string, unknown>, signal?: AbortSignal): Promise<OpenListMount>;
127
+ removeMount(id: string, signal?: AbortSignal): Promise<void>;
128
+ disableMount(id: string, disabled?: boolean, signal?: AbortSignal): Promise<void>;
129
+ configureManagedIndex(signal?: AbortSignal): Promise<void>;
130
+ updateIndex(mountPath: string, signal?: AbortSignal): Promise<void>;
131
+ reindexMount(_id?: string, signal?: AbortSignal): Promise<{
132
+ supported: true;
133
+ }>;
134
+ indexProgress(signal?: AbortSignal): Promise<Pick<OpenListMount, 'indexStatus' | 'indexProgress' | 'indexCount'> | undefined>;
135
+ private request;
136
+ private mountRows;
137
+ private storageRow;
138
+ private mountRow;
139
+ }
140
+ /** Owns only a process it launched; it never kills a user-managed OpenList. */
141
+ export declare class ManagedOpenListRuntime {
142
+ private readonly seams;
143
+ private child;
144
+ constructor(seams?: OpenListSeams);
145
+ get running(): boolean;
146
+ start(binary: string, dataDirectory: string, port?: number, signal?: AbortSignal): Promise<string>;
147
+ /** Generate an administrator password without exposing it in argv. */
148
+ setAdminPassword(binary: string, dataDirectory: string, _password?: string, signal?: AbortSignal, mutationState?: (state: OpenListAdminMutationState) => void): Promise<string>;
149
+ stop(): Promise<void>;
150
+ }
151
+ /** v4.2.2 reads `scheme.address` and `scheme.http_port` from data/config.json. */
152
+ export declare function writeManagedOpenListConfig(dataDirectory: string, port: number, seams?: OpenListSeams, signal?: AbortSignal): Promise<void>;
153
+ export declare function parseRandomAdminPassword(output: string): string | undefined;
154
+ export declare function isSensitiveDriverField(name: string): boolean;
155
+ export declare function isApiPagesQuickDriver(name: string): boolean;
156
+ /** Recover the only crash window in Windows' two-rename executable replacement. */
157
+ export declare function recoverInterruptedReplacement(target: string, backup: string, marker: string, seams?: OpenListSeams, windows?: boolean, owner?: symbol): Promise<void>;