dsh-session-recall 0.1.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 +21 -0
- package/README.md +104 -0
- package/README.zh.md +104 -0
- package/cordis.patch.yml +23 -0
- package/lib/index.d.ts +124 -0
- package/lib/index.js +390 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kittimzhe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# dsh-session-recall
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/dsh-session-recall) [](LICENSE)
|
|
6
|
+
|
|
7
|
+
Cross-session full-text recall for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): the model-facing `recall` tool lets the agent **search its own past session transcripts** — "that bug we fixed last week", "the font we chose for my resume" — through the trusted `ctx.sessionQuery` seam.
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
The official `@deepseek-ai/dsh-session-query` README lists exactly what is missing:
|
|
12
|
+
|
|
13
|
+
> **No registries or model-facing tool** — … a model-facing tool is absent.
|
|
14
|
+
> **No caller authorization** — … a model tool or UI must constrain which sessions its caller may inspect.
|
|
15
|
+
|
|
16
|
+
And the shipped web profile mounts its SQLite FTS5 backend with `openAt: never` and an in-memory database — so cross-session full-text search is off by default, and even when enabled the index dies with the process.
|
|
17
|
+
|
|
18
|
+
| | shipped web profile | + this plugin |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| Model-facing search tool | absent | **`recall`** |
|
|
21
|
+
| FTS index | `openAt: never` (off) | **on, lazy (`first-search`)** |
|
|
22
|
+
| Index storage | `:memory:` (lost on restart) | **persistent `<DSH_HOME>/session-recall/index.db`** |
|
|
23
|
+
| Caller authorization | caller's responsibility | **cwd-scoped by default, explicit opt-out** |
|
|
24
|
+
|
|
25
|
+
Memory plugins extract structured notes with an LLM (lossy, costs tokens); `recall` searches the **original transcripts** — zero extraction, zero loss, works retroactively on day one.
|
|
26
|
+
|
|
27
|
+
## What the model gets
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
recall({ query }) → best-matching event per session, current project only
|
|
31
|
+
recall({ query, all_projects: true }) → search every session on the machine
|
|
32
|
+
recall({ query, session_id }) → search the events of one session
|
|
33
|
+
recall({ query, limit, cursor }) → page through results
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Each hit carries the session id, title (best-effort), date, and a match snippet; the result renders as a native search card in the Web UI (`SearchMatchesResultView`). Zero-hit CJK queries get a tokenizer hint: the FTS `unicode61` tokenizer indexes uninterrupted CJK runs as single tokens, so the tool teaches the model to retry with short space-separated keywords.
|
|
37
|
+
|
|
38
|
+
## Scoping (the authorization gap)
|
|
39
|
+
|
|
40
|
+
`sessionQuery` is trusted infrastructure — it can read every session. This tool therefore constrains each call itself:
|
|
41
|
+
|
|
42
|
+
- by default, `sessionFilters: [{ kind: 'cwd', values: [<calling agent's cwd>] }]` — only sessions started in the same project directory;
|
|
43
|
+
- `all_projects: true` widens the scope, and only if the deployment allows it (`allowAllProjects: false` disables the argument).
|
|
44
|
+
|
|
45
|
+
## Install (out-of-tree plugin)
|
|
46
|
+
|
|
47
|
+
From npm:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
dsh plugin --profile web add dsh-session-recall
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or from GitHub:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
dsh plugin --profile web add github:kittimzhe/dsh-session-recall
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Then add to the profile's `cordis.patch.yml`:
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
- insert:
|
|
63
|
+
- id: session-recall
|
|
64
|
+
name: 'dsh-session-recall'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The bundle's own patch layer turns the persistent index on (`session-query-sqlite` → `openAt: first-search`, `path: <DSH_HOME>/session-recall/index.db`); if you maintain your own override of that row, keep those two values.
|
|
68
|
+
|
|
69
|
+
## Configuration
|
|
70
|
+
|
|
71
|
+
Plugin row config (all optional):
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
- id: session-recall
|
|
75
|
+
name: 'dsh-session-recall'
|
|
76
|
+
config:
|
|
77
|
+
allowAllProjects: true # honor the tool's all_projects argument
|
|
78
|
+
defaultLimit: 5 # page size when the model omits limit (1..10)
|
|
79
|
+
maxLimit: 10 # largest accepted page size (1..25)
|
|
80
|
+
cjkHint: true # zero-hit CJK tokenizer workaround hint
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Failure behavior
|
|
84
|
+
|
|
85
|
+
Every failure returns a friendly `hint` instead of a raw exception: a disabled index explains the two config keys needed, a stale cursor tells the model to restart without one, an unknown `session_id` suggests discovering sessions first. Title enrichment is best-effort — a failed title batch degrades to untitled rows, never a failed search.
|
|
86
|
+
|
|
87
|
+
## Known limitations
|
|
88
|
+
|
|
89
|
+
- First search after startup walks the durable logs to build the index (the tool description warns the model); subsequent searches are incremental.
|
|
90
|
+
- `unicode61` matches whole tokens/phrases, not substrings — `AI` does not match `BRAID`. The zero-hit CJK hint mitigates the worst case; a substring fallback via `filterEvents()` is a possible v2.
|
|
91
|
+
- One process must own the index file (single-writer SQLite, per the official backend).
|
|
92
|
+
|
|
93
|
+
## Development
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
npm install
|
|
97
|
+
npm run typecheck # tsc --noEmit
|
|
98
|
+
npm test # vitest run
|
|
99
|
+
npm run bundle # tsdown → lib/
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
[MIT](LICENSE)
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# dsh-session-recall
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/dsh-session-recall) [](LICENSE)
|
|
6
|
+
|
|
7
|
+
DeepSeek Harness 的跨会话全文回忆插件:注册模型可调用的 `recall` 工具,让 agent 能**检索自己过往的会话原文**——"上周修的那个 bug"、"简历选的什么字体"——全部通过可信的 `ctx.sessionQuery` 缝完成。
|
|
8
|
+
|
|
9
|
+
## 为什么做这个
|
|
10
|
+
|
|
11
|
+
官方 `@deepseek-ai/dsh-session-query` 的 README 自己列出了缺口:
|
|
12
|
+
|
|
13
|
+
> **No registries or model-facing tool** — … a model-facing tool is absent.(没有模型可调用的工具)
|
|
14
|
+
> **No caller authorization** — … a model tool or UI must constrain which sessions its caller may inspect.(调用方授权留给了未来的工具层)
|
|
15
|
+
|
|
16
|
+
而且官方 web profile 把 SQLite FTS5 后端配置成 `openAt: never` + 内存库——跨会话全文检索默认关闭,就算手动开启,索引也随进程退出而消失。
|
|
17
|
+
|
|
18
|
+
| | 官方 web 默认 | 装本插件后 |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| 模型可调用的搜索工具 | 无 | **`recall`** |
|
|
21
|
+
| FTS 索引 | `openAt: never`(关闭) | **开启(懒加载 `first-search`)** |
|
|
22
|
+
| 索引存储 | `:memory:`(重启即失) | **持久化 `<DSH_HOME>/session-recall/index.db`** |
|
|
23
|
+
| 调用方授权 | 留给调用方 | **默认按 cwd 限定当前项目,显式放宽** |
|
|
24
|
+
|
|
25
|
+
记忆类插件用 LLM 提取结构化笔记(有损、费 token);`recall` 检索的是**原始对话记录**——零提取、零损失、装完当天就能搜全部历史。
|
|
26
|
+
|
|
27
|
+
## 模型看到什么
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
recall({ query }) → 每个会话返回最强命中事件,默认只搜当前项目
|
|
31
|
+
recall({ query, all_projects: true }) → 搜索本机全部会话
|
|
32
|
+
recall({ query, session_id }) → 只搜指定会话内的事件
|
|
33
|
+
recall({ query, limit, cursor }) → 翻页
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
每条命中带会话 id、标题(尽力补全)、日期、命中摘录;结果在 Web UI 里渲染成原生搜索卡片(`SearchMatchesResultView`)。CJK 查询零命中时会给出分词提示:FTS 的 `unicode61` 分词器把连续中文当成一个 token,工具会教模型改用空格分隔的短关键词重试。
|
|
37
|
+
|
|
38
|
+
## 授权边界(官方明确留给工具层的责任)
|
|
39
|
+
|
|
40
|
+
`sessionQuery` 是可信基础设施——它能读所有会话。所以本工具自己约束每一次调用:
|
|
41
|
+
|
|
42
|
+
- 默认注入 `sessionFilters: [{ kind: 'cwd', values: [<调用方 agent 的 cwd>] }]`——只搜同一项目目录下开始的会话;
|
|
43
|
+
- `all_projects: true` 才放宽到全机,且部署方可以用 `allowAllProjects: false` 直接禁用该参数。
|
|
44
|
+
|
|
45
|
+
## 安装(out-of-tree 插件)
|
|
46
|
+
|
|
47
|
+
从 npm:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
dsh plugin --profile web add dsh-session-recall
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
或从 GitHub:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
dsh plugin --profile web add github:kittimzhe/dsh-session-recall
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
然后在 profile 的 `cordis.patch.yml` 里加入:
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
- insert:
|
|
63
|
+
- id: session-recall
|
|
64
|
+
name: 'dsh-session-recall'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
插件自带 bundle patch 会开启持久化索引(`session-query-sqlite` → `openAt: first-search`、`path: <DSH_HOME>/session-recall/index.db`);如果你自己覆盖过该行,请保留这两个值。
|
|
68
|
+
|
|
69
|
+
## 配置
|
|
70
|
+
|
|
71
|
+
插件行 config(全部可选):
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
- id: session-recall
|
|
75
|
+
name: 'dsh-session-recall'
|
|
76
|
+
config:
|
|
77
|
+
allowAllProjects: true # 是否允许工具的 all_projects 参数
|
|
78
|
+
defaultLimit: 5 # 模型省略 limit 时的页大小(1..10)
|
|
79
|
+
maxLimit: 10 # 最大页大小(1..25)
|
|
80
|
+
cjkHint: true # CJK 零命中的分词提示开关
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## 失败行为
|
|
84
|
+
|
|
85
|
+
所有失败都返回友好的 `hint` 而不是裸异常:索引未开启会说明需要哪两个配置键;游标失效会告诉模型不带游标重开一次;`session_id` 不存在会建议先做跨会话搜索。标题补全是尽力而为——标题批量读取失败只降级为"无标题"行,绝不让搜索失败。
|
|
86
|
+
|
|
87
|
+
## 已知限制
|
|
88
|
+
|
|
89
|
+
- 启动后第一次搜索会扫全量日志建索引(工具描述里已警告模型);之后增量更新。
|
|
90
|
+
- `unicode61` 按完整 token/短语匹配,不支持子串——`AI` 匹配不到 `BRAID`。CJK 零命中提示缓解了最坏情况;基于 `filterEvents()` 的子串兜底是 v2 方向。
|
|
91
|
+
- 索引文件单进程独占(官方后端的单写者 SQLite 约束)。
|
|
92
|
+
|
|
93
|
+
## 开发
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
npm install
|
|
97
|
+
npm run typecheck # tsc --noEmit
|
|
98
|
+
npm test # vitest run
|
|
99
|
+
npm run bundle # tsdown → lib/
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## 许可
|
|
103
|
+
|
|
104
|
+
[MIT](LICENSE)
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# The dsh-session-recall bundle patch, applied after every shipped bundle
|
|
2
|
+
# layer (dsh-base, dsh-web-app) and before the user's own profile layer.
|
|
3
|
+
#
|
|
4
|
+
# 1. Turn the opt-in FTS index on and persist it on disk. The shipped web
|
|
5
|
+
# profile mounts `session-query-sqlite` with `openAt: never` and an
|
|
6
|
+
# in-memory database, so cross-session full-text search is off and any
|
|
7
|
+
# manually enabled index dies with the process. `openAt: first-search`
|
|
8
|
+
# keeps Node 22 startup output clean (the official web-app layer cites the
|
|
9
|
+
# same reason), and `dshHomePath` is the loader-provided `!!js` helper the
|
|
10
|
+
# official layers themselves use, so the index lands at
|
|
11
|
+
# `<DSH_HOME>/session-recall/index.db` whatever DSH_HOME resolves to.
|
|
12
|
+
# A profile that already pointed this row at its own persistent index can
|
|
13
|
+
# override either value in its own cordis.patch.yml, which composes later.
|
|
14
|
+
# 2. Mount this plugin's model-facing `recall` tool. Requires the `tools`
|
|
15
|
+
# and `sessionQuery` services, which dsh-base already provides.
|
|
16
|
+
- id: session-query-sqlite
|
|
17
|
+
config:
|
|
18
|
+
openAt: first-search
|
|
19
|
+
path: !!js dshHomePath('session-recall', 'index.db')
|
|
20
|
+
|
|
21
|
+
- insert:
|
|
22
|
+
- id: session-recall
|
|
23
|
+
name: 'dsh-session-recall'
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { ToolDefinition } from "@deepseek-ai/dsh-tools";
|
|
2
|
+
import { SessionEventSearchPage, SessionEventSearchRequest, SessionSearchExecContext, SessionSearchHit, SessionSearchPage, SessionSearchRequest, SessionTitleObservationResult } from "@deepseek-ai/dsh-session-query";
|
|
3
|
+
import { JsonValue } from "@deepseek-ai/dsh-session";
|
|
4
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
5
|
+
import { ContentBlock } from "@deepseek-ai/dsh-llm";
|
|
6
|
+
//#region src/config.d.ts
|
|
7
|
+
/** Plugin-row configuration for dsh-session-recall. */
|
|
8
|
+
interface RecallConfig {
|
|
9
|
+
/** Honor the tool's `all_projects` argument. Default `true`. */
|
|
10
|
+
allowAllProjects?: boolean;
|
|
11
|
+
/** Page size when the model omits `limit`. Default `5`. */
|
|
12
|
+
defaultLimit?: number;
|
|
13
|
+
/** Largest accepted page size. Default `10`. */
|
|
14
|
+
maxLimit?: number;
|
|
15
|
+
/** Teach the model the CJK tokenizer workaround on zero hits. Default `true`. */
|
|
16
|
+
cjkHint?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** Validated, fully defaulted configuration. */
|
|
19
|
+
interface NormalizedRecallConfig {
|
|
20
|
+
readonly allowAllProjects: boolean;
|
|
21
|
+
readonly defaultLimit: number;
|
|
22
|
+
readonly maxLimit: number;
|
|
23
|
+
readonly cjkHint: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** Default, clamp, and cross-check every optional field. */
|
|
26
|
+
declare function normalizeRecallConfig(config?: RecallConfig): NormalizedRecallConfig;
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/types.d.ts
|
|
29
|
+
/** Canonical value types for the `recall` tool's declared output. */
|
|
30
|
+
/** The strongest matching event of one session, as surfaced by the search backend. */
|
|
31
|
+
interface RecallBestMatch {
|
|
32
|
+
seq: number;
|
|
33
|
+
type: string;
|
|
34
|
+
time: number;
|
|
35
|
+
snippet: string;
|
|
36
|
+
}
|
|
37
|
+
/** One session hit: identity plus its strongest matching event. */
|
|
38
|
+
interface RecallItem {
|
|
39
|
+
sessionId: string;
|
|
40
|
+
id8: string;
|
|
41
|
+
title: string | null;
|
|
42
|
+
createdAt: number;
|
|
43
|
+
cwd: string | null;
|
|
44
|
+
live: boolean;
|
|
45
|
+
persisted: boolean;
|
|
46
|
+
bestMatch: RecallBestMatch;
|
|
47
|
+
}
|
|
48
|
+
/** How this call scoped the corpus. */
|
|
49
|
+
interface RecallScope {
|
|
50
|
+
cwd: string | null;
|
|
51
|
+
allProjects: boolean;
|
|
52
|
+
sessionId: string | null;
|
|
53
|
+
}
|
|
54
|
+
/** The `recall` tool's canonical JSON value, validated against the output schema. */
|
|
55
|
+
interface RecallResult {
|
|
56
|
+
query: string;
|
|
57
|
+
scope: RecallScope;
|
|
58
|
+
count: number;
|
|
59
|
+
hasMore: boolean;
|
|
60
|
+
items: RecallItem[];
|
|
61
|
+
nextCursor: string | null;
|
|
62
|
+
hint: string | null;
|
|
63
|
+
}
|
|
64
|
+
/** The typed model-facing arguments after schema validation. */
|
|
65
|
+
interface RecallArgs {
|
|
66
|
+
query: string;
|
|
67
|
+
session_id?: string;
|
|
68
|
+
all_projects?: boolean;
|
|
69
|
+
limit?: number;
|
|
70
|
+
cursor?: string;
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/tool.d.ts
|
|
74
|
+
/** The ctx.sessionQuery surface this tool consumes (structural, for testability). */
|
|
75
|
+
interface RecallQueryEngine {
|
|
76
|
+
searchSessions(request: SessionSearchRequest, exec?: SessionSearchExecContext): Promise<SessionSearchPage<SessionSearchHit>>;
|
|
77
|
+
searchEvents(request: SessionEventSearchRequest, exec?: SessionSearchExecContext): Promise<SessionEventSearchPage>;
|
|
78
|
+
readTitleSnapshots(sessionIds: readonly string[], signal?: AbortSignal): Promise<SessionTitleObservationResult[]>;
|
|
79
|
+
}
|
|
80
|
+
declare const RECALL_TOOL_DESCRIPTION: string;
|
|
81
|
+
/**
|
|
82
|
+
* Build the `recall` ToolDefinition around a concrete sessionQuery engine.
|
|
83
|
+
* Pure construction — no registration happens here.
|
|
84
|
+
*/
|
|
85
|
+
declare function createRecallTool(config?: RecallConfig, engine?: RecallQueryEngine): ToolDefinition;
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/render.d.ts
|
|
88
|
+
/**
|
|
89
|
+
* Render the compact Markdown the model reads: one header line, one line per
|
|
90
|
+
* session hit with its best-match event, and a continuation pointer.
|
|
91
|
+
*/
|
|
92
|
+
declare function renderRecallText(result: RecallResult): string;
|
|
93
|
+
/** Content-block projection required by the tool's output declaration. */
|
|
94
|
+
declare function recallContentBlocks(result: RecallResult): ContentBlock[];
|
|
95
|
+
/** Replayable presentation payload consumed by `presentResult` on the UI plane. */
|
|
96
|
+
declare function recallPresentationMeta(result: RecallResult): JsonValue;
|
|
97
|
+
/** The zero-hit CJK tokenizer hint, or `null` when not applicable. */
|
|
98
|
+
declare function cjkZeroHitHint(query: string, zeroHits: boolean, enabled: boolean): string | null;
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/util.d.ts
|
|
101
|
+
/** Small pure helpers shared by the recall tool and its renderers. */
|
|
102
|
+
/** Clamp `n` into the inclusive `[lo, hi]` range. */
|
|
103
|
+
declare function clamp(n: number, lo: number, hi: number): number;
|
|
104
|
+
/**
|
|
105
|
+
* Short session slug for humans: strips the web-profile `session-` prefix and
|
|
106
|
+
* keeps the first 8 characters of what remains.
|
|
107
|
+
*/
|
|
108
|
+
declare function id8(sessionId: string): string;
|
|
109
|
+
/** Format Unix epoch milliseconds as a local `YYYY-MM-DD` date. */
|
|
110
|
+
declare function formatDate(epochMs: number): string;
|
|
111
|
+
/** Whether `text` contains at least one CJK ideograph, kana, or hangul character. */
|
|
112
|
+
declare function hasCJK(text: string): boolean;
|
|
113
|
+
/** Collapse every whitespace run and trim; used to normalize model-supplied queries. */
|
|
114
|
+
declare function normalizeQuery(text: string): string;
|
|
115
|
+
/** First line of `text` with control characters stripped, clipped to `limit` code points. */
|
|
116
|
+
declare function firstLineClipped(text: string, limit: number): string;
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/index.d.ts
|
|
119
|
+
declare const name = "session-recall";
|
|
120
|
+
declare const inject: string[];
|
|
121
|
+
/** Plugin entry: mount the `recall` tool on the global tool registry. */
|
|
122
|
+
declare function apply(ctx: Context, config?: RecallConfig): void;
|
|
123
|
+
//#endregion
|
|
124
|
+
export { type NormalizedRecallConfig, RECALL_TOOL_DESCRIPTION, type RecallArgs, type RecallBestMatch, type RecallConfig, type RecallItem, type RecallQueryEngine, type RecallResult, type RecallScope, apply, cjkZeroHitHint, clamp, createRecallTool, firstLineClipped, formatDate, hasCJK, id8, inject, name, normalizeQuery, normalizeRecallConfig, recallContentBlocks, recallPresentationMeta, renderRecallText };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
2
|
+
import { SessionSearchCursor } from "@deepseek-ai/dsh-session-query";
|
|
3
|
+
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
4
|
+
function intIn(value, fallback, lo, hi) {
|
|
5
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
6
|
+
return Math.min(hi, Math.max(lo, Math.trunc(value)));
|
|
7
|
+
}
|
|
8
|
+
/** Default, clamp, and cross-check every optional field. */
|
|
9
|
+
function normalizeRecallConfig(config) {
|
|
10
|
+
const defaultLimit = intIn(config?.defaultLimit, 5, 1, 10);
|
|
11
|
+
return {
|
|
12
|
+
allowAllProjects: config?.allowAllProjects !== false,
|
|
13
|
+
defaultLimit,
|
|
14
|
+
maxLimit: Math.max(defaultLimit, intIn(config?.maxLimit, 10, 1, 25)),
|
|
15
|
+
cjkHint: config?.cjkHint !== false
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/util.ts
|
|
20
|
+
/** Small pure helpers shared by the recall tool and its renderers. */
|
|
21
|
+
/** Clamp `n` into the inclusive `[lo, hi]` range. */
|
|
22
|
+
function clamp(n, lo, hi) {
|
|
23
|
+
return Math.min(hi, Math.max(lo, n));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Short session slug for humans: strips the web-profile `session-` prefix and
|
|
27
|
+
* keeps the first 8 characters of what remains.
|
|
28
|
+
*/
|
|
29
|
+
function id8(sessionId) {
|
|
30
|
+
return (sessionId.startsWith("session-") ? sessionId.slice(8) : sessionId).slice(0, 8);
|
|
31
|
+
}
|
|
32
|
+
/** Format Unix epoch milliseconds as a local `YYYY-MM-DD` date. */
|
|
33
|
+
function formatDate(epochMs) {
|
|
34
|
+
const d = new Date(epochMs);
|
|
35
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
36
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
37
|
+
return `${d.getFullYear()}-${m}-${day}`;
|
|
38
|
+
}
|
|
39
|
+
const CJK_RE = /[\u{3400}-\u{4DBF}\u{4E00}-\u{9FFF}\u{F900}-\u{FAFF}\u{3040}-\u{30FF}\u{AC00}-\u{D7AF}\u{3000}-\u{303F}]/u;
|
|
40
|
+
/** Whether `text` contains at least one CJK ideograph, kana, or hangul character. */
|
|
41
|
+
function hasCJK(text) {
|
|
42
|
+
return CJK_RE.test(text);
|
|
43
|
+
}
|
|
44
|
+
/** Collapse every whitespace run and trim; used to normalize model-supplied queries. */
|
|
45
|
+
function normalizeQuery(text) {
|
|
46
|
+
return text.trim().replaceAll(/\s+/g, " ");
|
|
47
|
+
}
|
|
48
|
+
/** First line of `text` with control characters stripped, clipped to `limit` code points. */
|
|
49
|
+
function firstLineClipped(text, limit) {
|
|
50
|
+
const line = text.split("\n", 1)[0] ?? "";
|
|
51
|
+
let out = "";
|
|
52
|
+
for (const ch of line) {
|
|
53
|
+
if ((ch.codePointAt(0) ?? 0) < 32) continue;
|
|
54
|
+
out += ch;
|
|
55
|
+
if (out.length >= limit) break;
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/render.ts
|
|
61
|
+
const SNIPPET_CHARS = 120;
|
|
62
|
+
function sessionLabel(result, index) {
|
|
63
|
+
const item = result.items[index];
|
|
64
|
+
if (!item) return String(index + 1);
|
|
65
|
+
return `${item.title ? firstLineClipped(item.title, 48) : `untitled ${item.id8}`} (${item.id8}, ${formatDate(item.createdAt)}${item.live ? ", live" : ""})`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Render the compact Markdown the model reads: one header line, one line per
|
|
69
|
+
* session hit with its best-match event, and a continuation pointer.
|
|
70
|
+
*/
|
|
71
|
+
function renderRecallText(result) {
|
|
72
|
+
const scopeParts = [];
|
|
73
|
+
if (result.scope.sessionId != null) scopeParts.push(`session ${result.scope.sessionId}`);
|
|
74
|
+
else if (result.scope.allProjects) scopeParts.push("all projects");
|
|
75
|
+
else if (result.scope.cwd != null) scopeParts.push(`cwd ${result.scope.cwd}`);
|
|
76
|
+
const lines = [];
|
|
77
|
+
const noun = result.scope.sessionId != null ? "events" : "sessions";
|
|
78
|
+
lines.push(`Recall "${result.query}" — ${result.count} ${noun}${scopeParts.length > 0 ? ` (${scopeParts.join(", ")})` : ""}`);
|
|
79
|
+
if (result.items.length === 0) lines.push("No matches.");
|
|
80
|
+
else result.items.forEach((item, i) => {
|
|
81
|
+
lines.push(`${i + 1}. ${sessionLabel(result, i)}`);
|
|
82
|
+
lines.push(` ${item.bestMatch.type} #${item.bestMatch.seq}: ${firstLineClipped(item.bestMatch.snippet, SNIPPET_CHARS)}`);
|
|
83
|
+
});
|
|
84
|
+
if (result.hasMore && result.nextCursor != null) lines.push(`More: re-call with cursor="${result.nextCursor}"`);
|
|
85
|
+
if (result.hint != null) lines.push(`Hint: ${result.hint}`);
|
|
86
|
+
return lines.join("\n");
|
|
87
|
+
}
|
|
88
|
+
/** Content-block projection required by the tool's output declaration. */
|
|
89
|
+
function recallContentBlocks(result) {
|
|
90
|
+
return [{
|
|
91
|
+
type: "text",
|
|
92
|
+
text: renderRecallText(result)
|
|
93
|
+
}];
|
|
94
|
+
}
|
|
95
|
+
/** Replayable presentation payload consumed by `presentResult` on the UI plane. */
|
|
96
|
+
function recallPresentationMeta(result) {
|
|
97
|
+
return {
|
|
98
|
+
query: result.query,
|
|
99
|
+
count: result.count,
|
|
100
|
+
hasMore: result.hasMore,
|
|
101
|
+
items: result.items.map((item) => ({
|
|
102
|
+
sessionId: item.sessionId,
|
|
103
|
+
id8: item.id8,
|
|
104
|
+
label: item.title ?? `untitled ${item.id8}`,
|
|
105
|
+
createdAt: item.createdAt,
|
|
106
|
+
seq: item.bestMatch.seq,
|
|
107
|
+
snippet: firstLineClipped(item.bestMatch.snippet, SNIPPET_CHARS)
|
|
108
|
+
}))
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** The zero-hit CJK tokenizer hint, or `null` when not applicable. */
|
|
112
|
+
function cjkZeroHitHint(query, zeroHits, enabled) {
|
|
113
|
+
if (!enabled || !zeroHits || !hasCJK(query)) return null;
|
|
114
|
+
return "no hits for a CJK query — the FTS unicode61 tokenizer indexes uninterrupted CJK runs as single tokens. Retry with short space-separated keywords (e.g. \"简历 模板\" instead of \"我的简历模板在这里\"), or an English term.";
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region src/tool.ts
|
|
118
|
+
/**
|
|
119
|
+
* The `recall` tool: model-facing cross-session full-text search over the
|
|
120
|
+
* trusted `ctx.sessionQuery` seam.
|
|
121
|
+
*
|
|
122
|
+
* The official `@deepseek-ai/dsh-session-query` service deliberately ships no
|
|
123
|
+
* model-facing tool ("No registries or model-facing tool — … a model-facing
|
|
124
|
+
* tool is absent") and no caller authorization ("a model tool or UI must
|
|
125
|
+
* constrain which sessions its caller may inspect"). This plugin fills both
|
|
126
|
+
* gaps: it registers one typed `recall` tool and constrains every call to the
|
|
127
|
+
* calling agent's own project cwd unless the model explicitly widens the
|
|
128
|
+
* scope and the deployment allows it.
|
|
129
|
+
*/
|
|
130
|
+
const RECALL_TOOL_DESCRIPTION = [
|
|
131
|
+
"Search the FULL TEXT of past and current session transcripts on this machine (your own conversation history with this user).",
|
|
132
|
+
"Use it when the user refers to earlier work (\"that bug we fixed last week\", \"the font we chose for my resume\") or when prior context was compacted away.",
|
|
133
|
+
"Matches whole words/phrases (English and code identifiers work best); returns the best-matching event snippet per session plus the session id.",
|
|
134
|
+
"Then use the read tool on files, or ask the user, to go deeper — this tool only points at history, it does not resume sessions.",
|
|
135
|
+
"Scoping: by default only sessions started in the current project directory; pass all_projects=true to search everywhere.",
|
|
136
|
+
"The first search after startup may be slow while the index builds."
|
|
137
|
+
].join(" ");
|
|
138
|
+
function errCode(error) {
|
|
139
|
+
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
|
|
140
|
+
}
|
|
141
|
+
function friendlyError(error) {
|
|
142
|
+
const code = errCode(error);
|
|
143
|
+
switch (code) {
|
|
144
|
+
case "SESSION_QUERY_SEARCH_DISABLED": return "full-text search is disabled for this deployment: the session-query backend row needs a dedicated on-disk path and an openAt of 'first-search' or 'startup'. This plugin's bundle patch already sets that for shipped web profiles — if you overrode the session-query-sqlite row yourself, check its config.";
|
|
145
|
+
case "SESSION_QUERY_INVALID_CURSOR":
|
|
146
|
+
case "SESSION_QUERY_STALE_CURSOR": return "the cursor is stale (the index moved on). Start a fresh recall call without a cursor.";
|
|
147
|
+
case "SESSION_QUERY_SESSION_NOT_FOUND": return "no session with that session_id exists. Search without session_id to discover candidate sessions first.";
|
|
148
|
+
case "SESSION_QUERY_INVALID_QUERY": return "the query was empty after trimming. Pass a non-empty search phrase.";
|
|
149
|
+
case "SESSION_QUERY_INVALID_LIMIT": return "the limit was out of range. Pass an integer between 1 and the deployment max (default 10).";
|
|
150
|
+
case "SESSION_QUERY_ABORTED": return "the search was cancelled before it finished.";
|
|
151
|
+
default: return `session search failed${code != null ? ` (${code})` : ""}: ${error instanceof Error ? error.message : String(error)}`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function recallError(query, error) {
|
|
155
|
+
return {
|
|
156
|
+
query,
|
|
157
|
+
scope: {
|
|
158
|
+
cwd: null,
|
|
159
|
+
allProjects: false,
|
|
160
|
+
sessionId: null
|
|
161
|
+
},
|
|
162
|
+
count: 0,
|
|
163
|
+
hasMore: false,
|
|
164
|
+
items: [],
|
|
165
|
+
nextCursor: null,
|
|
166
|
+
hint: friendlyError(error)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function brand(value) {
|
|
170
|
+
return value == null || value === "" ? void 0 : SessionSearchCursor(value);
|
|
171
|
+
}
|
|
172
|
+
/** Best-effort title enrichment; a failed batch degrades to untitled rows. */
|
|
173
|
+
async function titlesFor(engine, sessionIds, signal) {
|
|
174
|
+
const map = /* @__PURE__ */ new Map();
|
|
175
|
+
if (sessionIds.length === 0) return map;
|
|
176
|
+
try {
|
|
177
|
+
const results = await engine.readTitleSnapshots(sessionIds, signal);
|
|
178
|
+
for (const r of results) map.set(r.sessionId, r.status === "fulfilled" ? r.value.title?.title ?? null : null);
|
|
179
|
+
} catch {}
|
|
180
|
+
return map;
|
|
181
|
+
}
|
|
182
|
+
function toItems(hits, titles) {
|
|
183
|
+
return hits.map((hit) => ({
|
|
184
|
+
sessionId: hit.header.id,
|
|
185
|
+
id8: id8(hit.header.id),
|
|
186
|
+
title: titles.get(hit.header.id) ?? null,
|
|
187
|
+
createdAt: hit.header.createdAt,
|
|
188
|
+
cwd: hit.header.cwd ?? null,
|
|
189
|
+
live: hit.live,
|
|
190
|
+
persisted: hit.persisted,
|
|
191
|
+
bestMatch: {
|
|
192
|
+
seq: hit.bestMatch.seq,
|
|
193
|
+
type: hit.bestMatch.type,
|
|
194
|
+
time: hit.bestMatch.time,
|
|
195
|
+
snippet: hit.bestMatch.snippet
|
|
196
|
+
}
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
function eventItems(page, sessionId) {
|
|
200
|
+
return page.items.map((event) => ({
|
|
201
|
+
sessionId,
|
|
202
|
+
id8: id8(sessionId),
|
|
203
|
+
title: null,
|
|
204
|
+
createdAt: page.session.createdAt,
|
|
205
|
+
cwd: page.session.cwd ?? null,
|
|
206
|
+
live: true,
|
|
207
|
+
persisted: false,
|
|
208
|
+
bestMatch: {
|
|
209
|
+
seq: event.seq,
|
|
210
|
+
type: event.type,
|
|
211
|
+
time: event.time,
|
|
212
|
+
snippet: event.snippet
|
|
213
|
+
}
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
const nullableString = { oneOf: [{ type: "string" }, { type: "null" }] };
|
|
217
|
+
const recallOutputSchema = {
|
|
218
|
+
type: "object",
|
|
219
|
+
additionalProperties: false,
|
|
220
|
+
properties: {
|
|
221
|
+
query: { type: "string" },
|
|
222
|
+
scope: {
|
|
223
|
+
type: "object",
|
|
224
|
+
additionalProperties: false,
|
|
225
|
+
properties: {
|
|
226
|
+
cwd: nullableString,
|
|
227
|
+
allProjects: { type: "boolean" },
|
|
228
|
+
sessionId: nullableString
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
count: { type: "integer" },
|
|
232
|
+
hasMore: { type: "boolean" },
|
|
233
|
+
items: {
|
|
234
|
+
type: "array",
|
|
235
|
+
items: {
|
|
236
|
+
type: "object",
|
|
237
|
+
additionalProperties: false,
|
|
238
|
+
properties: {
|
|
239
|
+
sessionId: { type: "string" },
|
|
240
|
+
id8: { type: "string" },
|
|
241
|
+
title: nullableString,
|
|
242
|
+
createdAt: { type: "integer" },
|
|
243
|
+
cwd: nullableString,
|
|
244
|
+
live: { type: "boolean" },
|
|
245
|
+
persisted: { type: "boolean" },
|
|
246
|
+
bestMatch: {
|
|
247
|
+
type: "object",
|
|
248
|
+
additionalProperties: false,
|
|
249
|
+
properties: {
|
|
250
|
+
seq: { type: "integer" },
|
|
251
|
+
type: { type: "string" },
|
|
252
|
+
time: { type: "integer" },
|
|
253
|
+
snippet: { type: "string" }
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
nextCursor: nullableString,
|
|
260
|
+
hint: nullableString
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
/**
|
|
264
|
+
* Build the `recall` ToolDefinition around a concrete sessionQuery engine.
|
|
265
|
+
* Pure construction — no registration happens here.
|
|
266
|
+
*/
|
|
267
|
+
function createRecallTool(config, engine) {
|
|
268
|
+
const cfg = normalizeRecallConfig(config);
|
|
269
|
+
return defineTool({
|
|
270
|
+
name: "recall",
|
|
271
|
+
description: RECALL_TOOL_DESCRIPTION,
|
|
272
|
+
timeoutMs: 1e4,
|
|
273
|
+
isConcurrencySafe: () => true,
|
|
274
|
+
parameters: {
|
|
275
|
+
query: {
|
|
276
|
+
type: "string",
|
|
277
|
+
required: true,
|
|
278
|
+
description: "Literal phrase to search for in past transcript text (word/phrase match, not regex; FTS operators are treated as data)."
|
|
279
|
+
},
|
|
280
|
+
session_id: {
|
|
281
|
+
type: "string",
|
|
282
|
+
description: "Restrict the search to events of this one session (use after a cross-session hit pointed you here)."
|
|
283
|
+
},
|
|
284
|
+
all_projects: {
|
|
285
|
+
type: "boolean",
|
|
286
|
+
description: "Search sessions from ALL project directories instead of only the current one. Ignored when the deployment disallows it."
|
|
287
|
+
},
|
|
288
|
+
limit: {
|
|
289
|
+
type: "integer",
|
|
290
|
+
description: `Page size, default ${cfg.defaultLimit}, at most ${cfg.maxLimit}.`
|
|
291
|
+
},
|
|
292
|
+
cursor: {
|
|
293
|
+
type: "string",
|
|
294
|
+
description: "Opaque continuation cursor from a previous recall result with hasMore=true."
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
output: {
|
|
298
|
+
schema: recallOutputSchema,
|
|
299
|
+
render: (_args, value) => recallContentBlocks(value),
|
|
300
|
+
presentationMeta: (_args, value) => recallPresentationMeta(value)
|
|
301
|
+
},
|
|
302
|
+
async execute(args, exec) {
|
|
303
|
+
const query = normalizeQuery(args.query);
|
|
304
|
+
if (engine == null) return recallError(query, /* @__PURE__ */ new Error("no sessionQuery engine was wired into this plugin instance"));
|
|
305
|
+
if (query === "") return recallError(query, Object.assign(/* @__PURE__ */ new Error("empty query"), { code: "SESSION_QUERY_INVALID_QUERY" }));
|
|
306
|
+
const limit = clamp(Math.trunc(args.limit ?? cfg.defaultLimit), 1, cfg.maxLimit);
|
|
307
|
+
const agentCwd = exec.agent?.session.header?.cwd ?? null;
|
|
308
|
+
const wantAll = args.all_projects === true && cfg.allowAllProjects;
|
|
309
|
+
const scope = {
|
|
310
|
+
cwd: agentCwd,
|
|
311
|
+
allProjects: wantAll,
|
|
312
|
+
sessionId: args.session_id ?? null
|
|
313
|
+
};
|
|
314
|
+
try {
|
|
315
|
+
if (args.session_id != null && args.session_id !== "") {
|
|
316
|
+
const sessionId = SessionId(args.session_id);
|
|
317
|
+
const page = await engine.searchEvents({
|
|
318
|
+
sessionId,
|
|
319
|
+
query,
|
|
320
|
+
limit,
|
|
321
|
+
cursor: brand(args.cursor)
|
|
322
|
+
}, { signal: exec.signal });
|
|
323
|
+
const items = eventItems(page, sessionId);
|
|
324
|
+
return {
|
|
325
|
+
query,
|
|
326
|
+
scope,
|
|
327
|
+
count: items.length,
|
|
328
|
+
hasMore: page.nextCursor != null,
|
|
329
|
+
items,
|
|
330
|
+
nextCursor: page.nextCursor ?? null,
|
|
331
|
+
hint: cjkZeroHitHint(query, items.length === 0, cfg.cjkHint)
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const request = {
|
|
335
|
+
query,
|
|
336
|
+
limit
|
|
337
|
+
};
|
|
338
|
+
if (!wantAll && agentCwd != null) request.sessionFilters = [{
|
|
339
|
+
kind: "cwd",
|
|
340
|
+
values: [agentCwd]
|
|
341
|
+
}];
|
|
342
|
+
if (args.cursor != null && args.cursor !== "") request.cursor = brand(args.cursor);
|
|
343
|
+
const page = await engine.searchSessions(request, { signal: exec.signal });
|
|
344
|
+
const titles = await titlesFor(engine, page.items.map((hit) => hit.header.id), exec.signal);
|
|
345
|
+
const items = toItems(page.items, titles);
|
|
346
|
+
return {
|
|
347
|
+
query,
|
|
348
|
+
scope,
|
|
349
|
+
count: items.length,
|
|
350
|
+
hasMore: page.nextCursor != null,
|
|
351
|
+
items,
|
|
352
|
+
nextCursor: page.nextCursor ?? null,
|
|
353
|
+
hint: cjkZeroHitHint(query, items.length === 0, cfg.cjkHint)
|
|
354
|
+
};
|
|
355
|
+
} catch (error) {
|
|
356
|
+
return recallError(query, error);
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
presentResult: (_args, result) => {
|
|
360
|
+
const meta = result.meta;
|
|
361
|
+
if (meta == null || !Array.isArray(meta.items)) return void 0;
|
|
362
|
+
return {
|
|
363
|
+
card: "search",
|
|
364
|
+
shape: "matches",
|
|
365
|
+
title: `recall "${meta.query}"`,
|
|
366
|
+
files: meta.items.map((item) => ({
|
|
367
|
+
path: `${item.label} · ${item.id8}`,
|
|
368
|
+
matches: [{
|
|
369
|
+
lineNumber: item.seq,
|
|
370
|
+
line: item.snippet
|
|
371
|
+
}]
|
|
372
|
+
})),
|
|
373
|
+
truncated: meta.hasMore,
|
|
374
|
+
total: meta.count
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
//#endregion
|
|
380
|
+
//#region src/index.ts
|
|
381
|
+
const name = "session-recall";
|
|
382
|
+
const inject = ["tools", "sessionQuery"];
|
|
383
|
+
/** Plugin entry: mount the `recall` tool on the global tool registry. */
|
|
384
|
+
function apply(ctx, config) {
|
|
385
|
+
ctx.effect(function* () {
|
|
386
|
+
yield ctx.tools.register(createRecallTool(config, ctx.sessionQuery));
|
|
387
|
+
}, "session-recall lifecycle");
|
|
388
|
+
}
|
|
389
|
+
//#endregion
|
|
390
|
+
export { RECALL_TOOL_DESCRIPTION, apply, cjkZeroHitHint, clamp, createRecallTool, firstLineClipped, formatDate, hasCJK, id8, inject, name, normalizeQuery, normalizeRecallConfig, recallContentBlocks, recallPresentationMeta, renderRecallText };
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-session-recall",
|
|
3
|
+
"description": "Cross-session full-text recall for DeepSeek Harness: the model-facing `recall` tool searches past session transcripts through ctx.sessionQuery",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "lib/index.js",
|
|
10
|
+
"types": "lib/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./lib/index.d.ts",
|
|
14
|
+
"default": "./lib/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"lib",
|
|
20
|
+
"README.md",
|
|
21
|
+
"README.zh.md",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"cordis.patch.yml"
|
|
24
|
+
],
|
|
25
|
+
"dsh": {
|
|
26
|
+
"bundle": {
|
|
27
|
+
"patch": "./cordis.patch.yml"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"keywords": [
|
|
32
|
+
"deepseek-harness",
|
|
33
|
+
"dsh",
|
|
34
|
+
"dsh-plugin",
|
|
35
|
+
"session",
|
|
36
|
+
"search",
|
|
37
|
+
"recall",
|
|
38
|
+
"memory",
|
|
39
|
+
"full-text-search"
|
|
40
|
+
],
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
43
|
+
"@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
|
|
44
|
+
"@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
|
|
45
|
+
"@deepseek-ai/dsh-session": "^0.1.1-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-session-query": "^0.1.1-rc.2",
|
|
47
|
+
"@deepseek-ai/dsh-tools": "^0.1.1-rc.2"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
51
|
+
"@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
|
|
52
|
+
"@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
|
|
53
|
+
"@deepseek-ai/dsh-session": "^0.1.1-rc.2",
|
|
54
|
+
"@deepseek-ai/dsh-session-query": "^0.1.1-rc.2",
|
|
55
|
+
"@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
|
|
56
|
+
"@types/js-yaml": "^4.0.9",
|
|
57
|
+
"@types/node": "^26.2.0",
|
|
58
|
+
"js-yaml": "^4.2.0",
|
|
59
|
+
"tsdown": "^0.22.14",
|
|
60
|
+
"typescript": "^7.0.2",
|
|
61
|
+
"vitest": "^4.1.11"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"bundle": "tsdown",
|
|
65
|
+
"watch": "tsdown --watch",
|
|
66
|
+
"typecheck": "tsc --noEmit",
|
|
67
|
+
"test": "vitest run"
|
|
68
|
+
}
|
|
69
|
+
}
|