dsh-caduceus 0.0.0-stage → 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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +95 -2
  3. package/cordis.patch.yml +14 -0
  4. package/dist/atomic-write.d.ts +6 -0
  5. package/dist/atomic-write.js +43 -0
  6. package/dist/backfill.d.ts +27 -0
  7. package/dist/backfill.js +71 -0
  8. package/dist/bridge/bridge.d.ts +24 -0
  9. package/dist/bridge/bridge.js +1127 -0
  10. package/dist/bridge/index.d.ts +7 -0
  11. package/dist/bridge/index.js +6 -0
  12. package/dist/bridge/scope-queue.d.ts +19 -0
  13. package/dist/bridge/scope-queue.js +33 -0
  14. package/dist/bridge/types.d.ts +137 -0
  15. package/dist/bridge/types.js +1 -0
  16. package/dist/commands/dispatch.d.ts +38 -0
  17. package/dist/commands/dispatch.js +33 -0
  18. package/dist/commands/harness.d.ts +27 -0
  19. package/dist/commands/harness.js +25 -0
  20. package/dist/commands/parse.d.ts +70 -0
  21. package/dist/commands/parse.js +111 -0
  22. package/dist/config.d.ts +81 -0
  23. package/dist/config.js +72 -0
  24. package/dist/doctor.d.ts +93 -0
  25. package/dist/doctor.js +323 -0
  26. package/dist/file-logger.d.ts +39 -0
  27. package/dist/file-logger.js +127 -0
  28. package/dist/gate.d.ts +52 -0
  29. package/dist/gate.js +48 -0
  30. package/dist/home.d.ts +38 -0
  31. package/dist/home.js +68 -0
  32. package/dist/index.d.ts +8 -0
  33. package/dist/index.js +265 -0
  34. package/dist/logger.d.ts +26 -0
  35. package/dist/logger.js +44 -0
  36. package/dist/observability.d.ts +41 -0
  37. package/dist/observability.js +81 -0
  38. package/dist/ownership.d.ts +35 -0
  39. package/dist/ownership.js +143 -0
  40. package/dist/persona/auto-sync.d.ts +29 -0
  41. package/dist/persona/auto-sync.js +37 -0
  42. package/dist/persona/blocks.d.ts +123 -0
  43. package/dist/persona/blocks.js +344 -0
  44. package/dist/persona/command.d.ts +20 -0
  45. package/dist/persona/command.js +90 -0
  46. package/dist/persona/index.d.ts +28 -0
  47. package/dist/persona/index.js +36 -0
  48. package/dist/render/cards.d.ts +77 -0
  49. package/dist/render/cards.js +128 -0
  50. package/dist/render/index.d.ts +10 -0
  51. package/dist/render/index.js +7 -0
  52. package/dist/render/stream.d.ts +51 -0
  53. package/dist/render/stream.js +120 -0
  54. package/dist/render/truncate.d.ts +30 -0
  55. package/dist/render/truncate.js +52 -0
  56. package/dist/selfcheck.d.ts +73 -0
  57. package/dist/selfcheck.js +356 -0
  58. package/dist/session-files.d.ts +8 -0
  59. package/dist/session-files.js +45 -0
  60. package/dist/session-key.d.ts +127 -0
  61. package/dist/session-key.js +454 -0
  62. package/dist/status.d.ts +84 -0
  63. package/dist/status.js +210 -0
  64. package/dist/transport/feishu-api.d.ts +47 -0
  65. package/dist/transport/feishu-api.js +1 -0
  66. package/dist/transport/index.d.ts +13 -0
  67. package/dist/transport/index.js +8 -0
  68. package/dist/transport/lark.d.ts +44 -0
  69. package/dist/transport/lark.js +162 -0
  70. package/dist/transport/normalize.d.ts +54 -0
  71. package/dist/transport/normalize.js +54 -0
  72. package/dist/transport/reliable.d.ts +35 -0
  73. package/dist/transport/reliable.js +118 -0
  74. package/dist/transport/types.d.ts +141 -0
  75. package/dist/transport/types.js +48 -0
  76. package/package.json +113 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ArtomYuan
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 CHANGED
@@ -1,3 +1,96 @@
1
- # Temporary Holding Version
1
+ # dsh-caduceus
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 把 **DeepSeek Harness(dsh)** 接到 **飞书 / Lark** 的插件:一个话题一个会话,人格块自动同步,过程卡片承载状态与答案,并带 `/status`、`/doctor`、`/sessions` 等命令。
4
+
5
+ ## 安装
6
+
7
+ ### 路径 1:一次性命令行(推荐)
8
+
9
+ 发布到 npm 后,装包时直接放行依赖构建脚本:
10
+
11
+ ```bash
12
+ dsh plugin --profile web add dsh-caduceus --allow-build=protobufjs
13
+ ```
14
+
15
+ ### 路径 2:先授权再装(不带 flag 也能装)
16
+
17
+ 在目标 profile 目录的 `pnpm-workspace.yaml` 里加:
18
+
19
+ ```yaml
20
+ onlyBuiltDependencies:
21
+ - protobufjs
22
+ ```
23
+
24
+ 然后正常安装即可:
25
+
26
+ ```bash
27
+ dsh plugin --profile web add dsh-caduceus
28
+ ```
29
+
30
+ ### 路径 3:本地 tgz
31
+
32
+ ```bash
33
+ npm pack
34
+ dsh plugin --profile web add ./dsh-caduceus-0.1.0.tgz --allow-build=protobufjs
35
+ ```
36
+
37
+ ### 安装坑(实测 pnpm 11.7.0)
38
+
39
+ - **flag 必须用等号**:`--allow-build=protobufjs` 可以;`--allow-build protobufjs`(空格)会报
40
+ `ERR_PNPM_ALLOW_BUILD_MISSING_PACKAGE` 并安装失败。
41
+ - **为什么需要放行**:`protobufjs` 是飞书 SDK 的传递依赖;pnpm ≥10 默认不执行依赖包的安装/构建脚本,
42
+ 未放行时安装会以 `ERR_PNPM_IGNORED_BUILDS: Ignored build scripts: protobufjs@7.6.6` 失败。
43
+ - **确认装上了**:安装后执行
44
+
45
+ ```bash
46
+ dsh --profile web --dump-config | grep dsh-caduceus
47
+ ```
48
+
49
+ 应至少输出 2 行(两条 caduceus entry)。
50
+
51
+ 安装/启用后由宿主统一重启 dsh;本包不控制服务。
52
+
53
+ ## 配置
54
+
55
+ 在 profile 的 bundle 配置里提供:
56
+
57
+ | 配置 | 默认 | 说明 |
58
+ |:--|:--|:--|
59
+ | `appId` / `appSecret` | — | 飞书应用凭证;也可用 `FEISHU_APP_ID` / `FEISHU_APP_SECRET` |
60
+ | `defaultCwd` | 当前进程 cwd | 新建会话的工作目录 |
61
+ | `groupSessionsPerUser` | `true` | 普通群按发送者隔离会话;话题不受影响 |
62
+ | `busyMode` | `queue` | 群里忙时的投递:`queue` / `steer` / `interrupt` |
63
+ | `cardThrottleMs` | `400` | 过程卡 patch 节流(毫秒) |
64
+ | `personaAutoSync` | `true` | 启动时把 SOUL 同步到 `$DSH_HOME/AGENTS.md` |
65
+ | `agentPreset` | `standard` | 新会话挂载的 dsh agent preset |
66
+ | `logLevel` | `info` | 文件日志级别:`info` / `debug` |
67
+
68
+ 人格:SOUL 源按 `$DSH_HOME/SOUL.md` → `$DSH_AGENTS_HOME/SOUL.md` 查找,投影到 `$DSH_HOME/AGENTS.md` 的标记块;命令见下。
69
+
70
+ ## 命令
71
+
72
+ | 命令 | 说明 |
73
+ |:--|:--|
74
+ | `/new` | 立即创建新会话(旧会话保留) |
75
+ | `/sessions [--all]` | 列出当前会话或同聊天全部会话(含历史) |
76
+ | `/rename <标题>` | 重命名当前会话 |
77
+ | `/resume [id\|尾号]` | 恢复上一条或按完整 id / ≥8 位尾号切换 |
78
+ | `/adopt <id\|尾号>` | 把无归属的历史会话收养到当前 scope |
79
+ | `/selfcheck` | 立即重跑自检并输出逐项明细 |
80
+ | `/doctor [--json]` | 体检报告:运行时 / 飞书 / 人格 / 会话 / 存储 / 版本 |
81
+ | `/stop` | 中断正在运行的一轮 |
82
+ | `/status` | 当前会话状态与自检摘要 |
83
+ | `/persona [show\|sync]` | 查看或同步 SOUL 人格块 |
84
+ | `/help` | 命令表 |
85
+
86
+ ## 故障排查
87
+
88
+ - 启动日志里没有 `feishu long connection started`:先发 `/doctor` 看飞书凭证/长连接状态,再查文件日志。
89
+ - 先发 `/doctor`:逐项看到 preset、工具数、人格投影、飞书连接、存储等状态;`/doctor --json` 可给脚本或他人排查。
90
+ - 文件日志:`$DSH_HOME/caduceus/caduceus.log`(自动轮转,默认保留 3 份)。
91
+ - 自检异常会同时在对话里发一条 `⚠ 自检异常 …(详情 /doctor)`;恢复后会发 `✓ 自检已恢复`。
92
+ - 常见项:`preset:applied` 失败 = agent 没挂 preset(无工具/无人格);`persona:fresh` 失败 = SOUL 与 `AGENTS.md` 不一致,发 `/persona sync` 或重启等自动同步。
93
+
94
+ ## 许可
95
+
96
+ MIT。见 [LICENSE](LICENSE)。
@@ -0,0 +1,14 @@
1
+ # dsh-caduceus bundle patch (design doc §7).
2
+ #
3
+ # One package, two self-owned entries. `dsh plugin add` is the only install step.
4
+ #
5
+ # NOTE (2026-10-31, verified on 0.2.1-rc.2): the base profile ALREADY mounts
6
+ # `session-title` plus `session-title-llm` (first-prompt provider). Inserting a
7
+ # second provider row here registers the same provider id twice and fails at
8
+ # load, so this bundle adds no title row — the surface only calls
9
+ # `sessionTitle.rename()` / `.get()`.
10
+ - insert:
11
+ - id: caduceus-gateway
12
+ name: 'dsh-caduceus'
13
+ - id: caduceus-persona
14
+ name: 'dsh-caduceus/persona'
@@ -0,0 +1,6 @@
1
+ /** The sibling temporary path used before the rename. */
2
+ export declare function temporaryPathFor(filePath: string): string;
3
+ /** Synchronous flavour (session store: small, called from sync code paths). */
4
+ export declare function writeFileAtomicSync(filePath: string, data: string): void;
5
+ /** Asynchronous flavour (persona sync: called from async command handlers). */
6
+ export declare function writeFileAtomic(filePath: string, data: string): Promise<void>;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Small atomic-write helper shared by the session map and the persona block.
3
+ *
4
+ * Both files must survive a crash between "open" and "close": write the whole
5
+ * payload to a sibling temporary file, then rename it over the target. Rename
6
+ * is atomic on POSIX, so readers see either the old or the new file, never a
7
+ * torn one.
8
+ *
9
+ * @module dsh-caduceus/atomic-write
10
+ */
11
+ import { mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
12
+ import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
13
+ import { dirname } from 'node:path';
14
+ /** The sibling temporary path used before the rename. */
15
+ export function temporaryPathFor(filePath) {
16
+ return `${filePath}.tmp`;
17
+ }
18
+ /** Synchronous flavour (session store: small, called from sync code paths). */
19
+ export function writeFileAtomicSync(filePath, data) {
20
+ mkdirSync(dirname(filePath), { recursive: true });
21
+ const tempPath = temporaryPathFor(filePath);
22
+ try {
23
+ writeFileSync(tempPath, data, { encoding: 'utf8', mode: 0o600 });
24
+ renameSync(tempPath, filePath);
25
+ }
26
+ catch (error) {
27
+ rmSync(tempPath, { force: true });
28
+ throw error;
29
+ }
30
+ }
31
+ /** Asynchronous flavour (persona sync: called from async command handlers). */
32
+ export async function writeFileAtomic(filePath, data) {
33
+ await mkdir(dirname(filePath), { recursive: true });
34
+ const tempPath = temporaryPathFor(filePath);
35
+ try {
36
+ await writeFile(tempPath, data, { encoding: 'utf8', mode: 0o600 });
37
+ await rename(tempPath, filePath);
38
+ }
39
+ catch (error) {
40
+ await rm(tempPath, { force: true });
41
+ throw error;
42
+ }
43
+ }
@@ -0,0 +1,27 @@
1
+ import { type HomeEnv } from './home.js';
2
+ import type { LoggerLike } from './logger.js';
3
+ import { type OwnershipOptions } from './ownership.js';
4
+ import type { SessionStore } from './session-key.js';
5
+ export interface SessionDirEntry {
6
+ readonly sessionId: string;
7
+ readonly dir: string;
8
+ }
9
+ export interface BackfillOptions {
10
+ /** Sessions root; defaults to `<DSH_HOME>/sessions`. */
11
+ readonly root?: string;
12
+ readonly env?: HomeEnv;
13
+ readonly ownership?: OwnershipOptions;
14
+ readonly logger?: LoggerLike;
15
+ }
16
+ export interface BackfillResult {
17
+ readonly scanned: number;
18
+ readonly adopted: number;
19
+ readonly skipped: number;
20
+ }
21
+ /** Discover caduceus-minted session directories (`feishu-*`), sorted for determinism. */
22
+ export declare function scanSessionDirs(options?: BackfillOptions): SessionDirEntry[];
23
+ /**
24
+ * Adopt discovered sessions into their owning scope key's history.
25
+ * Idempotent: a second run adopts 0.
26
+ */
27
+ export declare function backfillSessionOwnership(store: SessionStore, options?: BackfillOptions): BackfillResult;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Startup ownership backfill: match dsh session directories to the caduceus
3
+ * ownership sidecar (or an ownership-carrying plain JSONL header) and append
4
+ * the session id to that scope key's history, so old sessions stay reachable
5
+ * even after a restart / history loss.
6
+ *
7
+ * Read-only scan of `<DSH_HOME>/sessions/<bucket>/<sessionId>/`; never creates
8
+ * agents or sessions. Session-map writes are the only side effect.
9
+ *
10
+ * @module dsh-caduceus/backfill
11
+ */
12
+ import { readdirSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import { dshHome } from './home.js';
15
+ import { silentLogger } from './logger.js';
16
+ import { readOwnership, readOwnershipFromSessionDir, } from './ownership.js';
17
+ /** Discover caduceus-minted session directories (`feishu-*`), sorted for determinism. */
18
+ export function scanSessionDirs(options = {}) {
19
+ const root = options.root ?? join(dshHome(options.env), 'sessions');
20
+ const found = [];
21
+ let buckets;
22
+ try {
23
+ buckets = readdirSync(root, { withFileTypes: true })
24
+ .filter((entry) => entry.isDirectory())
25
+ .map((entry) => entry.name)
26
+ .sort();
27
+ }
28
+ catch {
29
+ return found;
30
+ }
31
+ for (const bucket of buckets) {
32
+ let entries;
33
+ try {
34
+ entries = readdirSync(join(root, bucket), { withFileTypes: true })
35
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith('feishu-'))
36
+ .map((entry) => entry.name)
37
+ .sort();
38
+ }
39
+ catch {
40
+ continue;
41
+ }
42
+ for (const sessionId of entries) {
43
+ found.push({ sessionId, dir: join(root, bucket, sessionId) });
44
+ }
45
+ }
46
+ return found;
47
+ }
48
+ /**
49
+ * Adopt discovered sessions into their owning scope key's history.
50
+ * Idempotent: a second run adopts 0.
51
+ */
52
+ export function backfillSessionOwnership(store, options = {}) {
53
+ const logger = options.logger ?? silentLogger();
54
+ const ownershipOptions = options.ownership ?? {};
55
+ let scanned = 0;
56
+ let adopted = 0;
57
+ let skipped = 0;
58
+ for (const entry of scanSessionDirs(options)) {
59
+ scanned += 1;
60
+ const meta = readOwnership(entry.sessionId, ownershipOptions) ??
61
+ readOwnershipFromSessionDir(entry.dir);
62
+ if (meta === undefined) {
63
+ skipped += 1;
64
+ continue;
65
+ }
66
+ if (store.appendHistory(meta.scopeKey, entry.sessionId))
67
+ adopted += 1;
68
+ }
69
+ logger.info(`backfill: scanned=${scanned} adopted=${adopted} skipped=${skipped}`);
70
+ return { scanned, adopted, skipped };
71
+ }
@@ -0,0 +1,24 @@
1
+ import { type StatusEventLike, type StatusSnapshot } from '../status.js';
2
+ import type { FeishuCard, InboundMessage } from '../transport/types.js';
3
+ import type { BridgeOptions } from './types.js';
4
+ /** Extract the assistant's text from one `assistant/message` payload. */
5
+ export declare function assistantTextFromEvent(data: unknown): string;
6
+ /** Extract the raw `turn/end` reason kind. */
7
+ export declare function turnEndReasonKind(data: unknown): string | undefined;
8
+ /** Extract the accepted title from a `session/title` event payload. */
9
+ export declare function titleFromEvent(data: unknown): string | undefined;
10
+ /** The bridge. Construct one per gateway entry. */
11
+ export declare class Bridge {
12
+ #private;
13
+ constructor(options: BridgeOptions);
14
+ /** True when a session id belongs to a scope this bridge renders. */
15
+ ownsSession(sessionId: string): boolean;
16
+ /** Process one inbound message (serialized per scope). */
17
+ handleInbound(message: InboundMessage): Promise<void>;
18
+ /** Feed one dsh session event into the render/status layer. */
19
+ handleSessionEvent(session: {
20
+ readonly id: string;
21
+ }, event: StatusEventLike): Promise<void>;
22
+ }
23
+ /** Build the process card for one snapshot (exported for render tests). */
24
+ export declare function progressCardForSnapshot(snapshot: StatusSnapshot, sessionTitle: string | undefined, recentEvent: string, tail: string, now: number, presetMissing?: boolean): FeishuCard;