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.
- package/LICENSE +22 -0
- package/NOTICE.md +54 -0
- package/README.md +228 -0
- package/cordis.patch.yml +63 -0
- package/images/logo.png +0 -0
- package/lib/client/index.d.ts +3 -0
- package/lib/client.js +18151 -0
- package/lib/contract.d.ts +300 -0
- package/lib/contract.js +102 -0
- package/lib/errors.d.ts +56 -0
- package/lib/errors.js +21 -0
- package/lib/host.d.ts +102 -0
- package/lib/host.js +55 -0
- package/lib/index.d.ts +83 -0
- package/lib/index.js +153 -0
- package/lib/opencli.d.ts +116 -0
- package/lib/opencli.js +356 -0
- package/lib/reference.d.ts +49 -0
- package/lib/reference.js +228 -0
- package/lib/render.d.ts +101 -0
- package/lib/render.js +250 -0
- package/lib/retain.d.ts +35 -0
- package/lib/retain.js +116 -0
- package/lib/search.d.ts +75 -0
- package/lib/search.js +126 -0
- package/lib/serialize.d.ts +22 -0
- package/lib/serialize.js +24 -0
- package/lib/sources/file.d.ts +100 -0
- package/lib/sources/file.js +236 -0
- package/lib/sources/web-chat/index.d.ts +114 -0
- package/lib/sources/web-chat/index.js +359 -0
- package/lib/store/spec.d.ts +312 -0
- package/lib/store/spec.js +94 -0
- package/lib/store/store.d.ts +189 -0
- package/lib/store/store.js +638 -0
- package/lib/sync/index.d.ts +91 -0
- package/lib/sync/index.js +425 -0
- package/lib/tool.d.ts +62 -0
- package/lib/tool.js +364 -0
- package/lib/typert.d.ts +2 -0
- package/lib/typert.js +34 -0
- package/lib/types.d.ts +215 -0
- package/lib/types.js +12 -0
- package/lib/uri-codec.d.ts +5 -0
- package/lib/uri-codec.js +39 -0
- package/lib/uri.d.ts +59 -0
- package/lib/uri.js +86 -0
- package/lib/web.d.ts +4 -0
- package/lib/web.js +11 -0
- package/lib/window.d.ts +23 -0
- package/lib/window.js +35 -0
- package/lib/wire.d.ts +117 -0
- package/lib/wire.js +49 -0
- package/lib/workspace.d.ts +18 -0
- package/lib/workspace.js +81 -0
- package/opencli-plugin/chatgpt.js +125 -0
- package/opencli-plugin/claude.js +134 -0
- package/opencli-plugin/common.js +307 -0
- package/opencli-plugin/deepseek.js +152 -0
- package/opencli-plugin/gemini.js +165 -0
- package/opencli-plugin/grok.js +123 -0
- package/opencli-plugin/kimi.js +123 -0
- package/opencli-plugin/opencli-plugin.json +6 -0
- package/opencli-plugin/package.json +8 -0
- package/opencli-plugin/since-guard.js +45 -0
- package/package.json +180 -0
package/lib/uri-codec.js
ADDED
|
@@ -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
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
|
+
}
|
package/lib/window.d.ts
ADDED
|
@@ -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>;
|
package/lib/workspace.js
ADDED
|
@@ -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
|
+
})
|