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,39 @@
1
+ export const REFERENCE_SCHEME = 'dsh-ref:';
2
+ const PAYLOAD_PATTERN = /^[A-Za-z0-9_-]+$/u;
3
+ export function encodeReferenceUri(ref) {
4
+ const payload = encodeBase64Url(JSON.stringify({ source: ref.source, id: ref.id }));
5
+ return `${REFERENCE_SCHEME}${payload}`;
6
+ }
7
+ export function decodeReferenceUriUnchecked(uri) {
8
+ if (!uri.startsWith(REFERENCE_SCHEME))
9
+ throw new TypeError('wrong reference scheme');
10
+ const payload = uri.slice(REFERENCE_SCHEME.length);
11
+ if (!PAYLOAD_PATTERN.test(payload))
12
+ throw new TypeError('invalid base64url payload');
13
+ const parsed = JSON.parse(decodeBase64Url(payload));
14
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
15
+ throw new TypeError('decoded reference is not an object');
16
+ }
17
+ const { source, id } = parsed;
18
+ if (typeof source !== 'string' || typeof id !== 'string') {
19
+ throw new TypeError('decoded reference lacks string source and id');
20
+ }
21
+ const ref = { source, id };
22
+ if (encodeReferenceUri(ref) !== uri)
23
+ throw new TypeError('URI is not canonical');
24
+ return ref;
25
+ }
26
+ function encodeBase64Url(value) {
27
+ const bytes = new TextEncoder().encode(value);
28
+ let binary = '';
29
+ for (let offset = 0; offset < bytes.length; offset += 0x8000) {
30
+ binary += String.fromCharCode(...bytes.subarray(offset, Math.min(bytes.length, offset + 0x8000)));
31
+ }
32
+ return btoa(binary).replace(/\+/gu, '-').replace(/\//gu, '_').replace(/=+$/gu, '');
33
+ }
34
+ function decodeBase64Url(value) {
35
+ const base64 = value.replace(/-/gu, '+').replace(/_/gu, '/') + '='.repeat((4 - value.length % 4) % 4);
36
+ const binary = atob(base64);
37
+ const bytes = Uint8Array.from(binary, character => character.charCodeAt(0));
38
+ return new TextDecoder().decode(bytes);
39
+ }
package/lib/uri.d.ts ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Canonical `dsh-ref:` URI and the inline mention that carries it.
3
+ *
4
+ * A reference is two opaque strings, either of which may contain any character
5
+ * a JavaScript string can hold. JSON-encoding the pair before base64url means
6
+ * quotes, slashes, backslashes, newlines, and astral characters all survive
7
+ * without a delimiter the payload could itself spell.
8
+ *
9
+ * @module dsh-reference-anything/uri
10
+ */
11
+ import type { ReferenceInput, ReferenceRef } from './types.ts';
12
+ import { encodeReferenceUri, REFERENCE_SCHEME } from './uri-codec.ts';
13
+ export { encodeReferenceUri, REFERENCE_SCHEME };
14
+ /**
15
+ * Decode one canonical reference URI.
16
+ *
17
+ * Re-encodes the decoded value and compares: base64url has multiple spellings
18
+ * of the same bytes, and JSON has multiple spellings of the same object, so
19
+ * without this check two different URIs could name one reference and a
20
+ * consumer deduplicating by string would keep both.
21
+ * @param uri - complete URI, including the scheme.
22
+ * @returns the decoded reference.
23
+ */
24
+ export declare function decodeReferenceUri(uri: string): ReferenceRef;
25
+ /**
26
+ * Render a host-neutral Markdown mention carrying the canonical URI.
27
+ * @param ref - the reference to name.
28
+ * @param label - display text; falls back to the item id when empty.
29
+ * @returns an escaped `@[label](uri)` mention.
30
+ */
31
+ export declare function formatReferenceMention(ref: ReferenceRef, label?: string): string;
32
+ /** Readable text with mentions replaced, plus the references they named. */
33
+ export interface ParsedReferenceText {
34
+ /** The original text with every mention replaced by a readable `@label`. */
35
+ readonly text: string;
36
+ /** References in first-appearance order, before deduplication. */
37
+ readonly references: readonly ReferenceInput[];
38
+ }
39
+ /**
40
+ * Extract Markdown mentions and bare canonical URIs from one text value.
41
+ *
42
+ * An explicit `@[label](dsh-ref:…)` mention is a claim that this is a
43
+ * reference, so a malformed URI there is an error rather than prose. Bare text
44
+ * is only treated as a reference when it carries a base64url-shaped payload;
45
+ * a bare `dsh-ref:` with nothing after it stays ordinary discussion text, so
46
+ * writing *about* the scheme does not accidentally reference anything.
47
+ * @param text - the user's text.
48
+ * @returns readable text and the references it named, in order.
49
+ */
50
+ export declare function parseReferenceText(text: string): ParsedReferenceText;
51
+ /**
52
+ * Whether text could possibly contain a reference.
53
+ *
54
+ * The mention expander runs on every step of every turn, so the overwhelmingly
55
+ * common answer must cost one substring scan rather than a regex pass.
56
+ * @param text - the text to screen.
57
+ * @returns whether a full parse is worth running.
58
+ */
59
+ export declare function mayContainReference(text: string): boolean;
package/lib/uri.js ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Canonical `dsh-ref:` URI and the inline mention that carries it.
3
+ *
4
+ * A reference is two opaque strings, either of which may contain any character
5
+ * a JavaScript string can hold. JSON-encoding the pair before base64url means
6
+ * quotes, slashes, backslashes, newlines, and astral characters all survive
7
+ * without a delimiter the payload could itself spell.
8
+ *
9
+ * @module dsh-reference-anything/uri
10
+ */
11
+ import { ReferenceAnythingError } from "./errors.js";
12
+ import { decodeReferenceUriUnchecked, encodeReferenceUri, REFERENCE_SCHEME } from "./uri-codec.js";
13
+ export { encodeReferenceUri, REFERENCE_SCHEME };
14
+ /**
15
+ * Decode one canonical reference URI.
16
+ *
17
+ * Re-encodes the decoded value and compares: base64url has multiple spellings
18
+ * of the same bytes, and JSON has multiple spellings of the same object, so
19
+ * without this check two different URIs could name one reference and a
20
+ * consumer deduplicating by string would keep both.
21
+ * @param uri - complete URI, including the scheme.
22
+ * @returns the decoded reference.
23
+ */
24
+ export function decodeReferenceUri(uri) {
25
+ try {
26
+ return decodeReferenceUriUnchecked(uri);
27
+ }
28
+ catch (error) {
29
+ throw invalidUri(uri, error);
30
+ }
31
+ }
32
+ /**
33
+ * Render a host-neutral Markdown mention carrying the canonical URI.
34
+ * @param ref - the reference to name.
35
+ * @param label - display text; falls back to the item id when empty.
36
+ * @returns an escaped `@[label](uri)` mention.
37
+ */
38
+ export function formatReferenceMention(ref, label) {
39
+ const shown = label === undefined || label === '' ? ref.id : label;
40
+ return `@[${escapeLabel(shown)}](${encodeReferenceUri(ref)})`;
41
+ }
42
+ /**
43
+ * Extract Markdown mentions and bare canonical URIs from one text value.
44
+ *
45
+ * An explicit `@[label](dsh-ref:…)` mention is a claim that this is a
46
+ * reference, so a malformed URI there is an error rather than prose. Bare text
47
+ * is only treated as a reference when it carries a base64url-shaped payload;
48
+ * a bare `dsh-ref:` with nothing after it stays ordinary discussion text, so
49
+ * writing *about* the scheme does not accidentally reference anything.
50
+ * @param text - the user's text.
51
+ * @returns readable text and the references it named, in order.
52
+ */
53
+ export function parseReferenceText(text) {
54
+ const references = [];
55
+ const pattern = /@\[((?:\\.|[^\\\]])*)\]\((dsh-ref:[^\s)]*)\)|(dsh-ref:[A-Za-z0-9_-]+)/gu;
56
+ const rendered = text.replace(pattern, (_match, rawLabel, markdownUri, bareUri) => {
57
+ const uri = markdownUri ?? bareUri;
58
+ if (uri === undefined)
59
+ throw invalidUri(String(_match));
60
+ const ref = decodeReferenceUri(uri);
61
+ const label = rawLabel === undefined ? ref.id : unescapeLabel(rawLabel);
62
+ references.push({ ref, label });
63
+ return `@${label}`;
64
+ });
65
+ return { text: rendered, references };
66
+ }
67
+ /**
68
+ * Whether text could possibly contain a reference.
69
+ *
70
+ * The mention expander runs on every step of every turn, so the overwhelmingly
71
+ * common answer must cost one substring scan rather than a regex pass.
72
+ * @param text - the text to screen.
73
+ * @returns whether a full parse is worth running.
74
+ */
75
+ export function mayContainReference(text) {
76
+ return text.includes(REFERENCE_SCHEME);
77
+ }
78
+ function escapeLabel(label) {
79
+ return label.replace(/[\\\]]/gu, match => `\\${match}`);
80
+ }
81
+ function unescapeLabel(label) {
82
+ return label.replace(/\\(.)/gu, '$1');
83
+ }
84
+ function invalidUri(uri, cause) {
85
+ return new ReferenceAnythingError(`invalid reference URI ${JSON.stringify(uri)}`, 'REFERENCE_INVALID_URI', cause === undefined ? undefined : { cause });
86
+ }
package/lib/web.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ export declare const name = "reference-anything-web";
3
+ export declare const inject: string[];
4
+ export declare function apply(ctx: Context): void;
package/lib/web.js ADDED
@@ -0,0 +1,11 @@
1
+ import { ReferenceAnythingRemote } from "./host.js";
2
+ import { TYPERT_MANIFEST } from "./typert.js";
3
+ export const name = 'reference-anything-web';
4
+ export const inject = ['typert', 'referenceChatHistory', 'sessionReferenceResolver'];
5
+ export function apply(ctx) {
6
+ new ReferenceAnythingRemote(ctx);
7
+ ctx.effect(() => {
8
+ const dispose = ctx.typert.register(TYPERT_MANIFEST);
9
+ return () => { void dispose(); };
10
+ }, 'reference-anything-web.typert');
11
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Taking one window out of a conversation a source already holds in full.
3
+ *
4
+ * Sources that can read the whole conversation cheaply — a file, an export, an
5
+ * in-page payload — share this instead of each re-deriving the index
6
+ * arithmetic. A source that pages upstream computes its own slice and does not
7
+ * use this.
8
+ *
9
+ * @module dsh-reference-anything/window
10
+ */
11
+ import type { ConversationItem, ReferenceSlice, ReferenceWindow } from './types.ts';
12
+ /**
13
+ * Select the requested turns and say where they sit.
14
+ *
15
+ * `before` is clamped rather than rejected. A model reading backwards knows
16
+ * the index it last saw but not always the total, so asking for turns before a
17
+ * number past the end is an ordinary first request, not an error; the returned
18
+ * `startIndex` and `hasOlder` tell it where it actually landed.
19
+ * @param items - the complete conversation, oldest first.
20
+ * @param window - the requested turns.
21
+ * @returns the selected turns and their position.
22
+ */
23
+ export declare function sliceTurns(items: readonly ConversationItem[], window: ReferenceWindow): ReferenceSlice;
package/lib/window.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Taking one window out of a conversation a source already holds in full.
3
+ *
4
+ * Sources that can read the whole conversation cheaply — a file, an export, an
5
+ * in-page payload — share this instead of each re-deriving the index
6
+ * arithmetic. A source that pages upstream computes its own slice and does not
7
+ * use this.
8
+ *
9
+ * @module dsh-reference-anything/window
10
+ */
11
+ /**
12
+ * Select the requested turns and say where they sit.
13
+ *
14
+ * `before` is clamped rather than rejected. A model reading backwards knows
15
+ * the index it last saw but not always the total, so asking for turns before a
16
+ * number past the end is an ordinary first request, not an error; the returned
17
+ * `startIndex` and `hasOlder` tell it where it actually landed.
18
+ * @param items - the complete conversation, oldest first.
19
+ * @param window - the requested turns.
20
+ * @returns the selected turns and their position.
21
+ */
22
+ export function sliceTurns(items, window) {
23
+ const totalTurns = items.length;
24
+ const end = window.before === undefined
25
+ ? totalTurns
26
+ : Math.max(0, Math.min(Math.trunc(window.before), totalTurns));
27
+ const start = Math.max(0, end - Math.max(0, Math.trunc(window.limit)));
28
+ return {
29
+ kind: 'conversation',
30
+ items: items.slice(start, end),
31
+ startIndex: start,
32
+ totalTurns,
33
+ hasOlder: start > 0,
34
+ };
35
+ }
package/lib/wire.d.ts ADDED
@@ -0,0 +1,117 @@
1
+ /** JSON-compatible schema shared by the Host Remote and browser client. */
2
+ import { z } from 'zod';
3
+ export declare const providerSchema: z.ZodEnum<{
4
+ chatgpt: "chatgpt";
5
+ claude: "claude";
6
+ gemini: "gemini";
7
+ deepseek: "deepseek";
8
+ grok: "grok";
9
+ kimi: "kimi";
10
+ }>;
11
+ export type ChatProvider = z.infer<typeof providerSchema>;
12
+ export declare const ALL_PROVIDERS: readonly ChatProvider[];
13
+ export declare const pickerSourceSchema: z.ZodEnum<{
14
+ commands: "commands";
15
+ skills: "skills";
16
+ files: "files";
17
+ sessions: "sessions";
18
+ conversations: "conversations";
19
+ }>;
20
+ export type PickerSource = z.infer<typeof pickerSourceSchema>;
21
+ declare const pickerSourceSettingsSchema: z.ZodObject<{
22
+ enabled: z.ZodBoolean;
23
+ order: z.ZodNumber;
24
+ limit: z.ZodNumber;
25
+ }, z.core.$strip>;
26
+ export type PickerSourceSettings = z.infer<typeof pickerSourceSettingsSchema>;
27
+ export declare const pickerSettingsSchema: z.ZodObject<{
28
+ commands: z.ZodObject<{
29
+ enabled: z.ZodBoolean;
30
+ order: z.ZodNumber;
31
+ limit: z.ZodNumber;
32
+ }, z.core.$strip>;
33
+ skills: z.ZodObject<{
34
+ enabled: z.ZodBoolean;
35
+ order: z.ZodNumber;
36
+ limit: z.ZodNumber;
37
+ }, z.core.$strip>;
38
+ files: z.ZodObject<{
39
+ enabled: z.ZodBoolean;
40
+ order: z.ZodNumber;
41
+ limit: z.ZodNumber;
42
+ }, z.core.$strip>;
43
+ sessions: z.ZodObject<{
44
+ enabled: z.ZodBoolean;
45
+ order: z.ZodNumber;
46
+ limit: z.ZodNumber;
47
+ }, z.core.$strip>;
48
+ conversations: z.ZodObject<{
49
+ enabled: z.ZodBoolean;
50
+ order: z.ZodNumber;
51
+ limit: z.ZodNumber;
52
+ }, z.core.$strip>;
53
+ }, z.core.$strip>;
54
+ export type PickerSettings = z.infer<typeof pickerSettingsSchema>;
55
+ export declare const inputRenderModeSchema: z.ZodEnum<{
56
+ pill: "pill";
57
+ "raw-text": "raw-text";
58
+ }>;
59
+ export type InputRenderMode = z.infer<typeof inputRenderModeSchema>;
60
+ /** Defaults used before the user saves the General section. */
61
+ export declare function defaultPickerSettings(): PickerSettings;
62
+ /** Picker settings have a fixed schema, so explicit comparison is cheap and stable. */
63
+ export declare function samePickerSettings(left: PickerSettings, right: PickerSettings): boolean;
64
+ export declare const settingsRecordSchema: z.ZodObject<{
65
+ opencliPath: z.ZodString;
66
+ profile: z.ZodString;
67
+ detailConcurrency: z.ZodNumber;
68
+ autoSync: z.ZodDefault<z.ZodBoolean>;
69
+ syncOnStartup: z.ZodDefault<z.ZodBoolean>;
70
+ autoSyncMinutes: z.ZodDefault<z.ZodNumber>;
71
+ historyMode: z.ZodDefault<z.ZodEnum<{
72
+ "metadata-only": "metadata-only";
73
+ "offline-mirror": "offline-mirror";
74
+ }>>;
75
+ enabledProviders: z.ZodDefault<z.ZodArray<z.ZodEnum<{
76
+ chatgpt: "chatgpt";
77
+ claude: "claude";
78
+ gemini: "gemini";
79
+ deepseek: "deepseek";
80
+ grok: "grok";
81
+ kimi: "kimi";
82
+ }>>>;
83
+ maxReadTurns: z.ZodDefault<z.ZodNumber>;
84
+ inputRenderMode: z.ZodDefault<z.ZodEnum<{
85
+ pill: "pill";
86
+ "raw-text": "raw-text";
87
+ }>>;
88
+ picker: z.ZodOptional<z.ZodObject<{
89
+ commands: z.ZodObject<{
90
+ enabled: z.ZodBoolean;
91
+ order: z.ZodNumber;
92
+ limit: z.ZodNumber;
93
+ }, z.core.$strip>;
94
+ skills: z.ZodObject<{
95
+ enabled: z.ZodBoolean;
96
+ order: z.ZodNumber;
97
+ limit: z.ZodNumber;
98
+ }, z.core.$strip>;
99
+ files: z.ZodObject<{
100
+ enabled: z.ZodBoolean;
101
+ order: z.ZodNumber;
102
+ limit: z.ZodNumber;
103
+ }, z.core.$strip>;
104
+ sessions: z.ZodObject<{
105
+ enabled: z.ZodBoolean;
106
+ order: z.ZodNumber;
107
+ limit: z.ZodNumber;
108
+ }, z.core.$strip>;
109
+ conversations: z.ZodObject<{
110
+ enabled: z.ZodBoolean;
111
+ order: z.ZodNumber;
112
+ limit: z.ZodNumber;
113
+ }, z.core.$strip>;
114
+ }, z.core.$strip>>;
115
+ }, z.core.$strip>;
116
+ export type SettingsRecord = z.infer<typeof settingsRecordSchema>;
117
+ export {};
package/lib/wire.js ADDED
@@ -0,0 +1,49 @@
1
+ /** JSON-compatible schema shared by the Host Remote and browser client. */
2
+ import { z } from 'zod';
3
+ export const providerSchema = z.enum(['chatgpt', 'claude', 'gemini', 'deepseek', 'grok', 'kimi']);
4
+ export const ALL_PROVIDERS = providerSchema.options;
5
+ export const pickerSourceSchema = z.enum(['commands', 'skills', 'files', 'sessions', 'conversations']);
6
+ const pickerSourceSettingsSchema = z.object({
7
+ enabled: z.boolean(),
8
+ order: z.number().int().min(0).max(100),
9
+ limit: z.number().int().min(1).max(50),
10
+ });
11
+ export const pickerSettingsSchema = z.object({
12
+ commands: pickerSourceSettingsSchema,
13
+ skills: pickerSourceSettingsSchema,
14
+ files: pickerSourceSettingsSchema,
15
+ sessions: pickerSourceSettingsSchema,
16
+ conversations: pickerSourceSettingsSchema,
17
+ });
18
+ export const inputRenderModeSchema = z.enum(['pill', 'raw-text']);
19
+ /** Defaults used before the user saves the General section. */
20
+ export function defaultPickerSettings() {
21
+ return {
22
+ commands: { enabled: true, order: 0, limit: 6 },
23
+ skills: { enabled: true, order: 5, limit: 6 },
24
+ files: { enabled: true, order: 10, limit: 6 },
25
+ sessions: { enabled: true, order: 20, limit: 6 },
26
+ conversations: { enabled: true, order: 30, limit: 6 },
27
+ };
28
+ }
29
+ /** Picker settings have a fixed schema, so explicit comparison is cheap and stable. */
30
+ export function samePickerSettings(left, right) {
31
+ return Object.keys(left).every(key => left[key].enabled === right[key].enabled &&
32
+ left[key].order === right[key].order &&
33
+ left[key].limit === right[key].limit);
34
+ }
35
+ export const settingsRecordSchema = z.object({
36
+ opencliPath: z.string(),
37
+ profile: z.string(),
38
+ detailConcurrency: z.number().int().min(1).max(8),
39
+ autoSync: z.boolean().default(false),
40
+ syncOnStartup: z.boolean().default(false),
41
+ autoSyncMinutes: z.number().int().min(15).max(1440).default(60),
42
+ historyMode: z.enum(['metadata-only', 'offline-mirror']).default('metadata-only'),
43
+ enabledProviders: z.array(providerSchema).default([...ALL_PROVIDERS]),
44
+ maxReadTurns: z.number().int().min(1).max(100).default(10),
45
+ inputRenderMode: inputRenderModeSchema.default('pill'),
46
+ // Optional keeps existing on-disk settings forward-compatible. The client
47
+ // uses defaultPickerSettings() until the user saves the General section.
48
+ picker: pickerSettingsSchema.optional(),
49
+ });
@@ -0,0 +1,18 @@
1
+ import type { Agent } from '@deepseek-ai/dsh-agent';
2
+ export declare const WORKSPACE_REFERENCE_SCHEME = "dsh-file:";
3
+ export interface WorkspaceEntry {
4
+ path: string;
5
+ kind: 'file' | 'directory';
6
+ }
7
+ export interface WorkspaceReference {
8
+ path: string;
9
+ label: string;
10
+ }
11
+ export declare function indexWorkspace(agent: Agent, signal: AbortSignal, maxFiles?: number): Promise<WorkspaceEntry[]>;
12
+ export declare function encodeWorkspaceReferenceUri(path: string): string;
13
+ export declare function formatWorkspaceMention(reference: WorkspaceReference): string;
14
+ export declare function parseWorkspaceReferenceText(text: string): {
15
+ text: string;
16
+ references: WorkspaceReference[];
17
+ };
18
+ export declare function renderWorkspaceReferences(agent: Agent, references: readonly WorkspaceReference[]): Promise<string>;
@@ -0,0 +1,81 @@
1
+ import { lstat, readdir } from 'node:fs/promises';
2
+ import { basename, isAbsolute, relative, resolve, sep } from 'node:path';
3
+ export const WORKSPACE_REFERENCE_SCHEME = 'dsh-file:';
4
+ const IGNORED = new Set(['.git', '.hg', '.svn', 'node_modules', 'dist', 'build', 'coverage', '.next', '.cache', '.idea', '.vscode', '__pycache__']);
5
+ const IGNORED_FILES = new Set(['desktop.ini', 'Thumbs.db', '.DS_Store']);
6
+ export async function indexWorkspace(agent, signal, maxFiles = 5000) {
7
+ const root = agent.session.header.cwd;
8
+ if (!root || !isAbsolute(root))
9
+ return [];
10
+ const output = [];
11
+ const queue = ['.'];
12
+ while (queue.length > 0 && output.length < maxFiles) {
13
+ signal.throwIfAborted();
14
+ const current = queue.shift();
15
+ let entries;
16
+ try {
17
+ entries = await readdir(resolve(root, current), { withFileTypes: true });
18
+ }
19
+ catch {
20
+ continue;
21
+ }
22
+ entries.sort((a, b) => a.name.localeCompare(b.name));
23
+ for (const entry of entries) {
24
+ if (output.length >= maxFiles)
25
+ break;
26
+ if (entry.isSymbolicLink() || IGNORED_FILES.has(entry.name))
27
+ continue;
28
+ const path = current === '.' ? entry.name : `${current}/${entry.name}`;
29
+ if (entry.isDirectory()) {
30
+ if (IGNORED.has(entry.name))
31
+ continue;
32
+ output.push({ path, kind: 'directory' });
33
+ queue.push(path);
34
+ }
35
+ else if (entry.isFile())
36
+ output.push({ path, kind: 'file' });
37
+ }
38
+ }
39
+ return output;
40
+ }
41
+ export function encodeWorkspaceReferenceUri(path) {
42
+ return `${WORKSPACE_REFERENCE_SCHEME}${Buffer.from(JSON.stringify(path), 'utf8').toString('base64url')}`;
43
+ }
44
+ export function formatWorkspaceMention(reference) {
45
+ return `@[${reference.label.replace(/[\\\]]/gu, value => `\\${value}`)}](${encodeWorkspaceReferenceUri(reference.path)})`;
46
+ }
47
+ export function parseWorkspaceReferenceText(text) {
48
+ const references = [];
49
+ const pattern = /@\[((?:\\.|[^\\\]])*)\]\((dsh-file:[^\s)]*)\)|(dsh-file:[A-Za-z0-9_-]+)/gu;
50
+ const rendered = text.replace(pattern, (_match, rawLabel, markdownUri, bareUri) => {
51
+ const uri = markdownUri ?? bareUri;
52
+ const payload = uri.slice(WORKSPACE_REFERENCE_SCHEME.length);
53
+ if (!/^[A-Za-z0-9_-]+$/u.test(payload))
54
+ throw new Error(`invalid workspace reference URI ${JSON.stringify(uri)}`);
55
+ const path = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
56
+ if (typeof path !== 'string' || path.length === 0 || encodeWorkspaceReferenceUri(path) !== uri)
57
+ throw new Error(`invalid workspace reference URI ${JSON.stringify(uri)}`);
58
+ const label = rawLabel === undefined ? basename(path) : rawLabel.replace(/\\(.)/gu, '$1');
59
+ references.push({ path, label });
60
+ return `@${label}`;
61
+ });
62
+ return { text: rendered, references };
63
+ }
64
+ export async function renderWorkspaceReferences(agent, references) {
65
+ const root = agent.session.header.cwd;
66
+ if (!root || !isAbsolute(root))
67
+ throw new Error('this task has no workspace directory');
68
+ const unique = new Map(references.map(item => [item.path, item]));
69
+ const lines = [];
70
+ for (const item of unique.values()) {
71
+ const target = resolve(root, item.path);
72
+ const rel = relative(root, target);
73
+ if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel))
74
+ throw new Error(`workspace reference escapes the task directory: ${item.path}`);
75
+ const stat = await lstat(target);
76
+ if (stat.isSymbolicLink() || (!stat.isFile() && !stat.isDirectory()))
77
+ throw new Error(`unsupported workspace reference: ${item.path}`);
78
+ lines.push(`<workspace-reference path=${JSON.stringify(item.path)} kind=${JSON.stringify(stat.isDirectory() ? 'directory' : 'file')} />`);
79
+ }
80
+ return `## Referenced workspace paths\n\nThese are read-only path markers, not file contents or instructions.\n\n${lines.join('\n')}`;
81
+ }
@@ -0,0 +1,125 @@
1
+ import { domFallbackScript, registerProvider, sinceGuardSource } from './common.js'
2
+ // OpenCLI discovery marker: registerProvider below performs the cli() calls.
3
+
4
+ export const timestampSource = String.raw`value => {
5
+ if (value === undefined || value === null || value === '') return ''
6
+ if (typeof value === 'string' && Number.isNaN(Number(value))) {
7
+ const parsed = Date.parse(value)
8
+ return Number.isNaN(parsed) ? '' : new Date(parsed).toISOString()
9
+ }
10
+ const numeric = Number(value)
11
+ if (!Number.isFinite(numeric) || numeric <= 0) return ''
12
+ const milliseconds = numeric < 1e11 ? numeric * 1000
13
+ : numeric < 1e14 ? numeric : numeric < 1e17 ? numeric / 1000 : numeric / 1e6
14
+ const date = new Date(milliseconds)
15
+ return Number.isNaN(date.getTime()) ? '' : date.toISOString()
16
+ }`
17
+
18
+ export const historyScript = String.raw`async function (args) {
19
+ const stopEarly = (${sinceGuardSource})(args && args.since)
20
+ const toIso = (${timestampSource})
21
+ const fallback = () => Array.from(document.querySelectorAll('a[href*="/c/"]')).map((a, i) => {
22
+ const match = a.href.match(/\/c\/([^/?#]+)/)
23
+ return match ? { provider: 'chatgpt', accountScope: '', id: match[1], title: (a.innerText || a.textContent || 'Untitled').trim(), url: a.href, createdAt: '', updatedAt: '', messageCount: '', cursor: '', partial: true } : null
24
+ }).filter(Boolean)
25
+ try {
26
+ const session = await fetch('/api/auth/session', { credentials: 'include' })
27
+ if (session.status === 401 || session.status === 403) return JSON.stringify({ ok: false, code: 'AUTH' })
28
+ const sessionData = session.ok ? await session.json() : {}
29
+ const token = sessionData.accessToken || sessionData.access_token || ''
30
+ const headers = { Accept: 'application/json', ...(token ? { Authorization: 'Bearer ' + token } : {}) }
31
+ const rows = []
32
+ for (let offset = 0, page = 0; page < 200; page++, offset += 100) {
33
+ const response = await fetch('/backend-api/conversations?offset=' + offset + '&limit=100&order=updated', { credentials: 'include', headers })
34
+ if (response.status === 401 || response.status === 403) return JSON.stringify({ ok: false, code: 'AUTH' })
35
+ if (response.status === 429) return JSON.stringify({ ok: false, code: 'RATE_LIMIT' })
36
+ if (!response.ok) throw new Error('history HTTP ' + response.status)
37
+ const data = await response.json()
38
+ const items = data.items || data.conversations || []
39
+ const pageStart = rows.length
40
+ for (const item of items) rows.push({
41
+ provider: 'chatgpt', accountScope: '', id: String(item.id), title: item.title || 'Untitled Conversation',
42
+ url: location.origin + '/c/' + item.id,
43
+ createdAt: toIso(item.create_time),
44
+ updatedAt: toIso(item.update_time),
45
+ messageCount: item.message_count || item.messageCount || '', cursor: '', partial: false,
46
+ })
47
+ if (items.length < 100) return JSON.stringify({ ok: true, rows })
48
+ if (stopEarly(rows.slice(pageStart))) return JSON.stringify({ ok: true, rows })
49
+ }
50
+ return JSON.stringify({ ok: false, message: 'pagination exceeded 200 pages' })
51
+ } catch (error) {
52
+ const rows = fallback()
53
+ return JSON.stringify({ ok: rows.length > 0, rows, message: rows.length ? '' : String(error && error.message || error) })
54
+ }
55
+ }`
56
+
57
+ const detailScript = String.raw`async function (args) {
58
+ const attachment = (part, index) => {
59
+ if (!part || typeof part !== 'object') return null
60
+ const pointer = part.asset_pointer || part.file_id || part.id || ''
61
+ const rawUrl = part.url || part.download_url || ''
62
+ let locator = ''
63
+ try { if (rawUrl) { const u = new URL(rawUrl, location.origin); if (u.origin === location.origin && u.pathname !== '/') locator = u.pathname + u.search } } catch {}
64
+ if (!pointer && !locator) return null
65
+ return { attachmentId: String(pointer || locator || index), kind: part.content_type === 'image_asset_pointer' || /^image\//.test(part.mime_type || '') ? 'image' : 'file', name: part.name || part.file_name || '', mimeType: part.mime_type || '', size: part.size || null, locator, status: locator ? 'available' : 'unavailable' }
66
+ }
67
+ const partsOf = (msg) => {
68
+ const parts = Array.isArray(msg && msg.content && msg.content.parts) ? msg.content.parts : []
69
+ const text = parts.filter(p => typeof p === 'string').join('\n').trim()
70
+ const attachments = parts.map(attachment).filter(Boolean)
71
+ return { text, attachments }
72
+ }
73
+ try {
74
+ const session = await fetch('/api/auth/session', { credentials: 'include' })
75
+ const sessionData = session.ok ? await session.json() : {}
76
+ const token = sessionData.accessToken || sessionData.access_token || ''
77
+ const response = await fetch('/backend-api/conversation/' + encodeURIComponent(args.id), { credentials: 'include', headers: { Accept: 'application/json', ...(token ? { Authorization: 'Bearer ' + token } : {}) } })
78
+ if (response.status === 401 || response.status === 403) return JSON.stringify({ ok: false, code: 'AUTH' })
79
+ if (response.status === 429) return JSON.stringify({ ok: false, code: 'RATE_LIMIT' })
80
+ if (!response.ok) return JSON.stringify({ ok: false, message: 'detail HTTP ' + response.status })
81
+ const data = await response.json()
82
+ const mapping = data.mapping && typeof data.mapping === 'object' ? data.mapping : {}
83
+ const active = new Set()
84
+ let cursor = data.current_node
85
+ while (cursor && mapping[cursor] && !active.has(cursor)) { active.add(cursor); cursor = mapping[cursor].parent }
86
+ const rows = []
87
+ for (const [nodeId, node] of Object.entries(mapping)) {
88
+ const msg = node && node.message
89
+ const role = msg && msg.author && msg.author.role
90
+ if (role !== 'user' && role !== 'assistant') continue
91
+ const projected = partsOf(msg)
92
+ if (!projected.text && projected.attachments.length === 0) continue
93
+ rows.push({
94
+ conversationId: args.id, ordinal: 0, messageId: msg.id || nodeId,
95
+ parentId: node.parent || '', branchId: nodeId, activeBranch: active.has(nodeId),
96
+ role, text: projected.text,
97
+ createdAt: msg.create_time ? new Date(msg.create_time * 1000).toISOString() : '',
98
+ attachmentsJson: JSON.stringify(projected.attachments), partial: false,
99
+ })
100
+ }
101
+ const ordered = []
102
+ let nodeId = data.current_node
103
+ while (nodeId && mapping[nodeId]) { ordered.unshift(nodeId); nodeId = mapping[nodeId].parent }
104
+ const position = new Map(ordered.map((id, index) => [id, index]))
105
+ rows.sort((a, b) => (position.get(a.branchId) ?? 100000) - (position.get(b.branchId) ?? 100000))
106
+ rows.forEach((row, index) => { row.ordinal = index })
107
+ return JSON.stringify({ ok: rows.length > 0, rows, message: rows.length ? '' : 'mapping contained no messages' })
108
+ } catch (error) {
109
+ return JSON.stringify({ ok: false, message: String(error && error.message || error) })
110
+ }
111
+ }`
112
+
113
+ registerProvider({
114
+ site: 'dsh-chatgpt', provider: 'ChatGPT', domain: 'chatgpt.com', home: 'https://chatgpt.com/',
115
+ conversationUrl: id => `https://chatgpt.com/c/${encodeURIComponent(id)}`,
116
+ whoamiScript: String.raw`async function () {
117
+ const response = await fetch('/api/auth/session', { credentials: 'include' })
118
+ if (response.status === 401 || response.status === 403) return JSON.stringify({ ok: false, code: 'AUTH' })
119
+ if (!response.ok) return JSON.stringify({ ok: false, message: 'session HTTP ' + response.status })
120
+ const payload = await response.json(); const user = payload.user || payload.account || {}
121
+ const identity = String(user.id || user.account_id || user.email || '')
122
+ return JSON.stringify({ ok: Boolean(identity), identity, code: identity ? undefined : 'AUTH' })
123
+ }`,
124
+ historyScript, detailScript, fallbackScript: domFallbackScript,
125
+ })