dsh-theone 0.3.21 → 0.3.22

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.
@@ -0,0 +1,113 @@
1
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
2
+ import type { RouteView } from './types.ts';
3
+ /** Where reports go: a small Cloudflare Worker run by TheOne's maintainer (server/feedback). */
4
+ export declare const FEEDBACK_URL = "https://feedback.yulid.org/v1/reports";
5
+ export declare const FEEDBACK_EMAIL = "theone@yulid.org";
6
+ export declare const FEEDBACK_LIMITS: {
7
+ readonly description: 4000;
8
+ readonly contact: 200;
9
+ readonly routes: 30;
10
+ readonly replyText: 8000;
11
+ readonly body: 60000;
12
+ };
13
+ /** Strings as they may leave this computer: secrets, emails, addresses and the home folder removed. */
14
+ export declare function scrub(value: unknown, home?: string): unknown;
15
+ /** Recent routes without their text or topics: what happened, how, how long, and any error code. */
16
+ export declare function routeDigest(routes: readonly RouteView[], now?: number): {
17
+ corrected?: boolean | undefined;
18
+ error?: string | undefined;
19
+ ms?: number | undefined;
20
+ model?: string | undefined;
21
+ via?: "rules" | "llm" | undefined;
22
+ minutesAgo: number;
23
+ action: import("./types.ts").Action;
24
+ reason: string;
25
+ status: "planned" | "running" | "completed" | "failed";
26
+ }[];
27
+ /**
28
+ * One exchange in a session: the user message `messageId` (or, failing that, the latest user message
29
+ * with the same text) and every assistant text up to the next user message.
30
+ */
31
+ export declare function exchange(events: readonly SessionEvent[], messageId: string, sameText?: string): {
32
+ user: string;
33
+ reply: string;
34
+ steps: number;
35
+ } | undefined;
36
+ /**
37
+ * Whether main chat showed what the topic's own session said, and signs of garbled text in either:
38
+ * replacement characters (U+FFFD), stray control characters, and long runs of one repeated character.
39
+ */
40
+ export declare function compareReplies(main: string, worker: string): {
41
+ main: {
42
+ chars: number;
43
+ replacement: number;
44
+ control: number;
45
+ repeatedRuns: number;
46
+ };
47
+ worker: {
48
+ chars: number;
49
+ replacement: number;
50
+ control: number;
51
+ repeatedRuns: number;
52
+ };
53
+ firstDifference?: number | undefined;
54
+ identical: boolean;
55
+ };
56
+ /** The part of a report about one message: always the comparison, the texts only when the user agreed. */
57
+ export declare function replySection(route: RouteView, main: ReturnType<typeof exchange>, worker: ReturnType<typeof exchange>, includeText: boolean, now?: number): {
58
+ topicSessionReply?: string | undefined;
59
+ mainChatReply?: string | undefined;
60
+ message?: string | undefined;
61
+ comparison?: {
62
+ main: {
63
+ chars: number;
64
+ replacement: number;
65
+ control: number;
66
+ repeatedRuns: number;
67
+ };
68
+ worker: {
69
+ chars: number;
70
+ replacement: number;
71
+ control: number;
72
+ repeatedRuns: number;
73
+ };
74
+ firstDifference?: number | undefined;
75
+ identical: boolean;
76
+ } | undefined;
77
+ topicSteps?: number | undefined;
78
+ mainSteps?: number | undefined;
79
+ route: {
80
+ corrected?: boolean | undefined;
81
+ error?: string | undefined;
82
+ ms?: number | undefined;
83
+ model?: string | undefined;
84
+ via?: "rules" | "llm" | undefined;
85
+ minutesAgo: number;
86
+ action: import("./types.ts").Action;
87
+ reason: string;
88
+ status: "planned" | "running" | "completed" | "failed";
89
+ };
90
+ found: {
91
+ mainChat: boolean;
92
+ topicSession: boolean;
93
+ };
94
+ };
95
+ export interface FeedbackDraft {
96
+ v: 1;
97
+ app: 'theone';
98
+ version: string;
99
+ diagnostics: Record<string, unknown>;
100
+ reply?: Record<string, unknown>;
101
+ }
102
+ export interface FeedbackReport extends FeedbackDraft {
103
+ lang: 'zh' | 'en';
104
+ description: string;
105
+ contact?: string;
106
+ }
107
+ /**
108
+ * The report to send: the draft the user saw plus what they typed. The draft comes back from the
109
+ * page, so it is checked and scrubbed again here; anything malformed is refused, not repaired.
110
+ */
111
+ export declare function finalReport(input: Record<string, unknown>, version: string, home?: string): FeedbackReport;
112
+ /** Send a report; resolves to its id, or throws RATE_LIMITED, REJECTED, UNREACHABLE or SERVER_ERROR. */
113
+ export declare function sendReport(report: FeedbackReport, url?: string, fetcher?: typeof fetch): Promise<string>;
@@ -0,0 +1,146 @@
1
+ import { redactRoutingText } from "./routing-policy.js";
2
+ /** Where reports go: a small Cloudflare Worker run by TheOne's maintainer (server/feedback). */
3
+ export const FEEDBACK_URL = 'https://feedback.yulid.org/v1/reports';
4
+ export const FEEDBACK_EMAIL = 'theone@yulid.org';
5
+ export const FEEDBACK_LIMITS = { description: 4000, contact: 200, routes: 30, replyText: 8000, body: 60000 };
6
+ const MINUTE = 60000;
7
+ /** Strings as they may leave this computer: secrets, emails, addresses and the home folder removed. */
8
+ export function scrub(value, home) {
9
+ if (typeof value === 'string') {
10
+ let text = redactRoutingText(value);
11
+ if (home && home.length > 1)
12
+ text = text.split(home).join('~');
13
+ return text;
14
+ }
15
+ if (Array.isArray(value))
16
+ return value.map(item => scrub(item, home));
17
+ if (value && typeof value === 'object')
18
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, scrub(item, home)]));
19
+ return value;
20
+ }
21
+ /** A decision's reason when it is one of TheOne's own codes; a model's free-text reason may quote the message. */
22
+ function reasonCode(reason) {
23
+ return /^[a-z][\w:-]{0,59}$/i.test(reason) ? reason : 'model';
24
+ }
25
+ /** Recent routes without their text or topics: what happened, how, how long, and any error code. */
26
+ export function routeDigest(routes, now = Date.now()) {
27
+ return routes.slice(0, FEEDBACK_LIMITS.routes).map(route => ({
28
+ minutesAgo: Math.max(0, Math.round((now - route.at) / MINUTE)),
29
+ action: route.decision.action,
30
+ reason: reasonCode(route.decision.reason),
31
+ status: route.status,
32
+ ...(route.receipt ? { via: route.receipt.mode, ...(route.receipt.model ? { model: route.receipt.model } : {}),
33
+ ...(route.receipt.elapsedMs !== undefined ? { ms: route.receipt.elapsedMs } : {}), ...(route.receipt.errorCode ? { error: route.receipt.errorCode } : {}) } : {}),
34
+ ...(route.correctedTo ? { corrected: route.correctedTo !== route.decision.contextId } : {}),
35
+ }));
36
+ }
37
+ const blockText = (blocks) => blocks.flatMap(block => block.type === 'text' && block.text ? [block.text] : []).join('');
38
+ /**
39
+ * One exchange in a session: the user message `messageId` (or, failing that, the latest user message
40
+ * with the same text) and every assistant text up to the next user message.
41
+ */
42
+ export function exchange(events, messageId, sameText) {
43
+ let start = events.findIndex(event => event.type === 'user/message' && event.data.id === messageId);
44
+ if (start < 0 && sameText !== undefined) {
45
+ for (let index = events.length - 1; index >= 0; index--) {
46
+ const event = events[index];
47
+ if (event.type === 'user/message' && event.data.source.kind === 'user' && blockText(event.data.content).trim() === sameText.trim()) {
48
+ start = index;
49
+ break;
50
+ }
51
+ }
52
+ }
53
+ if (start < 0)
54
+ return undefined;
55
+ const first = events[start];
56
+ if (first.type !== 'user/message')
57
+ return undefined;
58
+ const replies = [];
59
+ for (const event of events.slice(start + 1)) {
60
+ if (event.type === 'user/message' && event.data.source.kind === 'user')
61
+ break;
62
+ if (event.type === 'assistant/message')
63
+ replies.push(blockText(event.data.message.content));
64
+ }
65
+ return { user: blockText(first.data.content), reply: replies.join(''), steps: replies.length };
66
+ }
67
+ const count = (text, pattern) => (text.match(pattern) ?? []).length;
68
+ /**
69
+ * Whether main chat showed what the topic's own session said, and signs of garbled text in either:
70
+ * replacement characters (U+FFFD), stray control characters, and long runs of one repeated character.
71
+ */
72
+ export function compareReplies(main, worker) {
73
+ let first = 0;
74
+ while (first < main.length && first < worker.length && main[first] === worker[first])
75
+ first++;
76
+ const identical = main === worker;
77
+ const signs = (text) => ({ chars: text.length, replacement: count(text, /�/g),
78
+ control: count(text, /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g), repeatedRuns: count(text, /(.)\1{19,}/gsu) });
79
+ return { identical, ...(identical ? {} : { firstDifference: first }), main: signs(main), worker: signs(worker) };
80
+ }
81
+ /** Text around the first difference, so a long reply still shows where the two part ways. */
82
+ function excerpt(text, around) {
83
+ if (text.length <= FEEDBACK_LIMITS.replyText)
84
+ return text;
85
+ const start = around === undefined ? 0 : Math.max(0, Math.min(text.length - FEEDBACK_LIMITS.replyText, around - FEEDBACK_LIMITS.replyText / 2));
86
+ return (start ? '…' : '') + text.slice(start, start + FEEDBACK_LIMITS.replyText) + (start + FEEDBACK_LIMITS.replyText < text.length ? '…' : '');
87
+ }
88
+ /** The part of a report about one message: always the comparison, the texts only when the user agreed. */
89
+ export function replySection(route, main, worker, includeText, now = Date.now()) {
90
+ const comparison = main && worker ? compareReplies(main.reply, worker.reply) : undefined;
91
+ return {
92
+ route: routeDigest([route], now)[0],
93
+ found: { mainChat: !!main, topicSession: !!worker },
94
+ ...(main ? { mainSteps: main.steps } : {}), ...(worker ? { topicSteps: worker.steps } : {}),
95
+ ...(comparison ? { comparison } : {}),
96
+ ...(includeText ? {
97
+ message: (main ?? worker)?.user.slice(0, 2000),
98
+ ...(main ? { mainChatReply: excerpt(main.reply, comparison?.firstDifference) } : {}),
99
+ ...(worker ? { topicSessionReply: excerpt(worker.reply, comparison?.firstDifference) } : {}),
100
+ } : {}),
101
+ };
102
+ }
103
+ /**
104
+ * The report to send: the draft the user saw plus what they typed. The draft comes back from the
105
+ * page, so it is checked and scrubbed again here; anything malformed is refused, not repaired.
106
+ */
107
+ export function finalReport(input, version, home) {
108
+ const draft = input.draft;
109
+ const description = typeof input.description === 'string' ? input.description.trim() : '';
110
+ const contact = typeof input.contact === 'string' ? input.contact.trim() : '';
111
+ if (!draft || typeof draft !== 'object' || draft.v !== 1 || draft.app !== 'theone')
112
+ throw new Error('INVALID_INPUT');
113
+ if (!draft.diagnostics || typeof draft.diagnostics !== 'object' || Array.isArray(draft.diagnostics))
114
+ throw new Error('INVALID_INPUT');
115
+ if (draft.reply != null && (typeof draft.reply !== 'object' || Array.isArray(draft.reply)))
116
+ throw new Error('INVALID_INPUT');
117
+ if (!description)
118
+ throw new Error('DESCRIPTION_REQUIRED');
119
+ if (description.length > FEEDBACK_LIMITS.description || contact.length > FEEDBACK_LIMITS.contact)
120
+ throw new Error('TOO_LARGE');
121
+ const report = { v: 1, app: 'theone', version, lang: input.lang === 'zh' ? 'zh' : 'en', description,
122
+ ...(contact ? { contact } : {}),
123
+ diagnostics: scrub(draft.diagnostics, home),
124
+ ...(draft.reply ? { reply: scrub(draft.reply, home) } : {}) };
125
+ if (JSON.stringify(report).length > FEEDBACK_LIMITS.body)
126
+ throw new Error('TOO_LARGE');
127
+ return report;
128
+ }
129
+ /** Send a report; resolves to its id, or throws RATE_LIMITED, REJECTED, UNREACHABLE or SERVER_ERROR. */
130
+ export async function sendReport(report, url = FEEDBACK_URL, fetcher = fetch) {
131
+ let response;
132
+ try {
133
+ response = await fetcher(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report), signal: AbortSignal.timeout(15000) });
134
+ }
135
+ catch {
136
+ throw new Error('UNREACHABLE');
137
+ }
138
+ if (response.status === 429)
139
+ throw new Error('RATE_LIMITED');
140
+ if (response.status >= 400 && response.status < 500)
141
+ throw new Error('REJECTED');
142
+ const body = await response.json().catch(() => undefined);
143
+ if (!response.ok || typeof body?.id !== 'string' || !/^FB-[A-Z0-9]{4,12}$/.test(body.id))
144
+ throw new Error('SERVER_ERROR');
145
+ return body.id;
146
+ }
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ import { type SettingsSnapshot } from './settings-types.ts';
11
11
  import { type LinkScope } from './linkage.ts';
12
12
  import { Updater } from './update.ts';
13
13
  import { NoticeBoard } from './notices.ts';
14
+ import { type FeedbackDraft } from './feedback.ts';
14
15
  export interface Config {
15
16
  databasePath?: string;
16
17
  contextsPath?: string;
@@ -39,6 +40,8 @@ export interface Config {
39
40
  topicCards?: boolean;
40
41
  /** Where notices are read from; for testing. */
41
42
  noticeUrl?: string;
43
+ /** Where problem reports are sent when the user presses Send; "off" leaves only copy and email. For testing. */
44
+ feedbackUrl?: string;
42
45
  }
43
46
  declare module '@deepseek-ai/cordis' {
44
47
  interface Context {
@@ -105,6 +108,12 @@ export default class TheOne extends Service {
105
108
  private editTopics;
106
109
  /** Existing DSH sessions a topic can take as history: not main chats and not topics' own Workers. */
107
110
  private attachableSessions;
111
+ /**
112
+ * What a problem report carries before the user adds their words: versions, settings and how
113
+ * routing went, without message text or topic names. With `messageId`, also how that message's
114
+ * reply in main chat compares with its topic session, and the texts themselves if `includeReply`.
115
+ */
116
+ feedbackDraft(messageId: string | undefined, includeReply: boolean): Promise<FeedbackDraft>;
108
117
  /** Read only public options; never read or return the API key environment value. */
109
118
  settingsSnapshot(): Promise<SettingsSnapshot>;
110
119
  private routerFor;
package/dist/index.js CHANGED
@@ -25,6 +25,8 @@ import { applyExtraction, EXTRACT_PROMPT, extractionPayload, factCandidates, fac
25
25
  import { activityLabel, CARD_AFTER_REPLIES, CARD_PROMPT, cardPayload, learnDormancy, parseCard } from "./topic-memory.js";
26
26
  import { PACKAGE_NAME, Updater } from "./update.js";
27
27
  import { NOTICE_URL, NoticeBoard } from "./notices.js";
28
+ import { exchange, FEEDBACK_EMAIL, FEEDBACK_LIMITS, FEEDBACK_URL, finalReport, replySection, routeDigest, scrub, sendReport } from "./feedback.js";
29
+ import { createRequire } from 'node:module';
28
30
  function redactDescriptor(text) {
29
31
  return text.replace(/\bsk-[a-zA-Z0-9_-]{16,}|\bBearer\s+\S+/gi, '[REDACTED]')
30
32
  .replace(/((?:api[_ -]?key|password|密码|密钥)\s*[:=:]\s*)\S+/gi, '$1[REDACTED]');
@@ -139,6 +141,15 @@ function readOwnVersion() {
139
141
  return '0.0.0';
140
142
  }
141
143
  }
144
+ /** DSH's own version, as far as its packages say; undefined when it cannot be read. */
145
+ function readDshVersion() {
146
+ try {
147
+ return String(createRequire(import.meta.url)('@deepseek-ai/dsh-agent/package.json').version ?? '') || undefined;
148
+ }
149
+ catch {
150
+ return undefined;
151
+ }
152
+ }
142
153
  function readDescriptors(path) {
143
154
  const value = JSON.parse(readFileSync(path, 'utf8'));
144
155
  if (!Array.isArray(value))
@@ -176,6 +187,7 @@ export default class TheOne extends Service {
176
187
  factExtraction: z.boolean().default(false),
177
188
  topicCards: z.boolean().default(true),
178
189
  noticeUrl: z.string(),
190
+ feedbackUrl: z.string(),
179
191
  });
180
192
  store;
181
193
  catalog;
@@ -697,6 +709,34 @@ export default class TheOne extends Service {
697
709
  const dismissed = this.store.dismissedNotices();
698
710
  return Response.json({ notices: (await this.noticeBoard.notices()).filter(notice => !dismissed.has(notice.id)) }, { headers: { 'cache-control': 'no-store' } });
699
711
  } }));
712
+ // Problem reports: a draft to show the user in full, then sent only when they press Send.
713
+ child.effect(() => connection.fetch.register({ path: '/api/theone/feedback', methods: ['POST'], requestBody: 'buffered', fetch: async (request) => {
714
+ let row;
715
+ try {
716
+ row = await request.json();
717
+ }
718
+ catch {
719
+ return Response.json({ error: 'INVALID_INPUT' }, { status: 400 });
720
+ }
721
+ const url = this.config.feedbackUrl || FEEDBACK_URL;
722
+ try {
723
+ if (row?.action === 'draft') {
724
+ const messageId = typeof row.messageId === 'string' ? row.messageId : undefined;
725
+ return Response.json({ draft: await this.feedbackDraft(messageId, row.includeReply === true), email: FEEDBACK_EMAIL, direct: url !== 'off' }, { headers: { 'cache-control': 'no-store' } });
726
+ }
727
+ if (row?.action === 'send') {
728
+ if (url === 'off')
729
+ return Response.json({ error: 'DIRECT_OFF' }, { status: 400 });
730
+ const report = finalReport(row, this.updater.current, homedir());
731
+ return Response.json({ id: await sendReport(report, url) }, { headers: { 'cache-control': 'no-store' } });
732
+ }
733
+ return Response.json({ error: 'INVALID_INPUT' }, { status: 400 });
734
+ }
735
+ catch (error) {
736
+ const code = error instanceof Error && /^[A-Z_]+$/.test(error.message) ? error.message : 'FEEDBACK_FAILED';
737
+ return Response.json({ error: code }, { status: ['UNREACHABLE', 'SERVER_ERROR', 'RATE_LIMITED', 'REJECTED'].includes(code) ? 502 : 400 });
738
+ }
739
+ } }));
700
740
  child.effect(() => connection.fetch.register({ path: '/api/theone/sessions', methods: ['GET'], requestBody: 'buffered', fetch: async () => Response.json({ sessions: await this.attachableSessions() }, { headers: { 'cache-control': 'no-store' } }) }));
701
741
  // The user's corrections to topic linking always take precedence over what was learned.
702
742
  child.effect(() => connection.fetch.register({ path: '/api/theone/links', methods: ['POST'], requestBody: 'buffered', fetch: async (request) => {
@@ -736,6 +776,9 @@ export default class TheOne extends Service {
736
776
  }
737
777
  if (!value || typeof value !== 'object' || !('contextId' in value) || typeof value.contextId !== 'string' || !this.store.contexts().some(c => c.id === value.contextId))
738
778
  return Response.json({ error: 'UNKNOWN_CONTEXT' }, { status: 400 });
779
+ // Its conversations are gone or archived: mounting it would only start an empty Worker.
780
+ if (this.store.hiddenReasons().has(value.contextId))
781
+ return Response.json({ error: 'TOPIC_HIDDEN' }, { status: 409 });
739
782
  if (this.active || this.reservedGateway)
740
783
  return Response.json({ error: 'GATEWAY_BUSY' }, { status: 409 });
741
784
  this.store.mount(this.config.gatewayKey, value.contextId);
@@ -830,6 +873,52 @@ export default class TheOne extends Service {
830
873
  return { id: record.header.id, title: title || new Date(record.header.createdAt).toISOString().slice(0, 16).replace('T', ' '), createdAt: record.header.createdAt };
831
874
  });
832
875
  }
876
+ /**
877
+ * What a problem report carries before the user adds their words: versions, settings and how
878
+ * routing went, without message text or topic names. With `messageId`, also how that message's
879
+ * reply in main chat compares with its topic session, and the texts themselves if `includeReply`.
880
+ */
881
+ async feedbackDraft(messageId, includeReply) {
882
+ const c = this.config;
883
+ const settings = await this.settingsSnapshot().catch(() => undefined);
884
+ const values = settings?.values;
885
+ const status = this.catalog?.snapshot().status;
886
+ const update = await this.updater.status().catch(() => undefined);
887
+ const routes = this.store.recentRoutes(c.gatewayKey, FEEDBACK_LIMITS.routes);
888
+ const diagnostics = {
889
+ theone: this.updater.current, dsh: readDshVersion() ?? null, node: process.version, platform: `${process.platform}-${process.arch}`,
890
+ model: settings?.model ? { provider: settings.model.provider, model: settings.model.model } : null,
891
+ ...(settings?.modelUnavailable ? { modelUnavailable: true } : {}),
892
+ settings: values ? { routerMode: values.routerMode, linkScope: values.linkScope, routeNotice: values.routeNotice, notices: values.notices,
893
+ historyCatalog: values.historyCatalog, factLinks: values.factLinks, factExtraction: values.factExtraction, topicCards: c.topicCards !== false,
894
+ maxResponseChars: values.maxResponseChars, maxDescriptorChars: values.maxDescriptorChars,
895
+ pinnedModel: !!(values.workerProvider && values.workerModel), manualCatalog: !!values.contextsPath } : null,
896
+ topics: { total: this.store.contexts().length, hidden: this.store.hiddenReasons().size, workspaces: this.store.groups().length },
897
+ catalog: status ? { running: status.running, scanned: status.scanned, indexed: status.indexed, skipped: status.skipped, failed: status.failed,
898
+ pending: status.pending, ...(status.searchUnavailable ? { searchUnavailable: true } : {}) } : { off: true },
899
+ routing: { stats: this.store.routeStats(c.gatewayKey), recent: routeDigest(routes) },
900
+ update: update ? { source: update.source, ...(update.latest ? { latest: update.latest } : {}), ...(update.state ? { state: update.state } : {}),
901
+ ...(update.error ? { error: update.error } : {}) } : null,
902
+ busy: this.active,
903
+ };
904
+ let reply;
905
+ if (messageId) {
906
+ const route = routes.find(item => item.messageId === messageId);
907
+ const stored = this.store.route(messageId);
908
+ if (!route || !stored || !this.store.isGateway(stored.gatewayId))
909
+ throw new Error('UNKNOWN_MESSAGE');
910
+ const read = (id) => this.ctx.sessionQuery.readSession(SessionId(id)).then(log => log.events, () => undefined);
911
+ const mainEvents = await read(stored.gatewayId);
912
+ const main = mainEvents && exchange(mainEvents, messageId);
913
+ const sessionId = this.store.contexts().find(context => context.id === route.decision.contextId)?.workingSessionId;
914
+ const workerEvents = sessionId ? await read(sessionId) : undefined;
915
+ const worker = workerEvents && exchange(workerEvents, messageId, main?.user);
916
+ reply = replySection(route, main, worker, includeReply);
917
+ }
918
+ // Scrubbed here too, so what the user reads is exactly what is sent.
919
+ return { v: 1, app: 'theone', version: this.updater.current, diagnostics: scrub(diagnostics, homedir()),
920
+ ...(reply ? { reply: scrub(reply, homedir()) } : {}) };
921
+ }
833
922
  /** Read only public options; never read or return the API key environment value. */
834
923
  async settingsSnapshot() {
835
924
  const c = this.config;
package/package.json CHANGED
@@ -1,9 +1,22 @@
1
1
  {
2
2
  "name": "dsh-theone",
3
- "version": "0.3.21",
3
+ "version": "0.3.22",
4
4
  "private": false,
5
5
  "type": "module",
6
- "description": "Automatic context routing for DeepSeek Harness",
6
+ "description": "One chat for everything in DeepSeek Harness (DSH): no more hunting for old conversations. Each message goes to the topic it belongs to, with its own context.",
7
+ "keywords": [
8
+ "deepseek-harness",
9
+ "dsh",
10
+ "dsh-plugin",
11
+ "deepseek",
12
+ "ai-chat",
13
+ "chat-organizer",
14
+ "conversation-management",
15
+ "chat-history",
16
+ "context-management",
17
+ "conversation-routing",
18
+ "productivity"
19
+ ],
7
20
  "main": "./dist/index.js",
8
21
  "types": "./dist/index.d.ts",
9
22
  "exports": {
@@ -57,7 +70,8 @@
57
70
  "pack:plugin": "npm run build && node scripts/pack-plugin.mjs",
58
71
  "install:local": "npm run build && node scripts/install-local.mjs",
59
72
  "start:local": "node scripts/start-local.mjs",
60
- "eval:test": "node --test eval/test/*.test.mjs && python3 -m unittest discover -s eval/test"
73
+ "eval:test": "node --test eval/test/*.test.mjs && python3 -m unittest discover -s eval/test",
74
+ "feedback:test": "node --test server/feedback/test/*.test.mjs"
61
75
  },
62
76
  "dependencies": {
63
77
  "@deepseek-ai/schemastery": "3.18.4"
@@ -106,5 +120,9 @@
106
120
  "repository": {
107
121
  "type": "git",
108
122
  "url": "git+https://github.com/YunongDai2005/dsh-theone.git"
123
+ },
124
+ "bugs": {
125
+ "url": "https://github.com/YunongDai2005/dsh-theone/issues",
126
+ "email": "theone@yulid.org"
109
127
  }
110
128
  }