hob-bot 1.0.3 → 1.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/README.md +75 -5
- package/README.zh-CN.md +75 -5
- package/dist/web/assets/DesktopView-rVxacveF.js +5 -0
- package/dist/web/assets/index-B-8ISOr1.js +267 -0
- package/dist/web/assets/index-Cu1Pf8yX.css +1 -0
- package/dist/web/assets/{math-zZ4JA8eM.js → math-weKGDUmL.js} +2 -2
- package/dist/web/index.html +2 -2
- package/dist/web/mock/desktop.jpg +0 -0
- package/dist/web/mock/photo.jpg +0 -0
- package/dist/web/mock/photo2.jpg +0 -0
- package/dist/web/mock/sunset.html +90 -0
- package/dist/web/mock//347/201/253/346/230/237/346/227/245/350/220/275/347/244/272/346/204/217/345/233/276.md +34 -0
- package/package.json +10 -2
- package/server/src/actions.ts +24 -57
- package/server/src/assistant-skills.ts +201 -0
- package/server/src/boot.ts +1 -0
- package/server/src/claude-status.ts +47 -0
- package/server/src/config.ts +3 -1
- package/server/src/delegations.ts +40 -12
- package/server/src/desktop-control.ts +152 -0
- package/server/src/desktop-events.ts +63 -0
- package/server/src/desktop-public.ts +43 -0
- package/server/src/desktop-tools.ts +161 -0
- package/server/src/desktop.ts +76 -0
- package/server/src/embedding.ts +52 -0
- package/server/src/files.ts +63 -43
- package/server/src/http.ts +334 -15
- package/server/src/jobs.ts +330 -59
- package/server/src/knowledge-index.ts +123 -0
- package/server/src/knowledge-tools.ts +94 -0
- package/server/src/knowledge.ts +451 -0
- package/server/src/lore-entry.ts +10 -0
- package/server/src/main.ts +6 -0
- package/server/src/mcp.ts +745 -0
- package/server/src/memory.ts +11 -4
- package/server/src/now.ts +33 -0
- package/server/src/overview.ts +1 -1
- package/server/src/pages.ts +1 -1
- package/server/src/pi.ts +92 -16
- package/server/src/prompt.ts +46 -154
- package/server/src/providers.ts +3 -2
- package/server/src/push.ts +42 -7
- package/server/src/router-activity.ts +4 -0
- package/server/src/router-tools.ts +132 -38
- package/server/src/router.ts +159 -19
- package/server/src/routines.ts +14 -4
- package/server/src/runtime.ts +15 -0
- package/server/src/settings.ts +43 -1
- package/server/src/store.ts +4 -3
- package/server/src/thumbs.ts +49 -0
- package/server/src/transcript.ts +57 -4
- package/server/src/worker-tools.ts +14 -12
- package/shared/knowledge.ts +92 -0
- package/shared/mcp.ts +91 -0
- package/shared/media.ts +6 -0
- package/shared/plain-text.ts +31 -0
- package/shared/skills.ts +18 -0
- package/shared/sprites.ts +198 -0
- package/shared/types.ts +105 -20
- package/vendor/claude-bridge/HOB-PATCH.md +10 -0
- package/vendor/claude-bridge/LICENSE +21 -0
- package/vendor/claude-bridge/package.json +62 -0
- package/vendor/claude-bridge/src/agents-md.ts +14 -0
- package/vendor/claude-bridge/src/askclaude-schema.ts +92 -0
- package/vendor/claude-bridge/src/askclaude-ui.ts +90 -0
- package/vendor/claude-bridge/src/attachments.ts +135 -0
- package/vendor/claude-bridge/src/config.ts +105 -0
- package/vendor/claude-bridge/src/convert.ts +222 -0
- package/vendor/claude-bridge/src/extract-tool-results.ts +47 -0
- package/vendor/claude-bridge/src/index.ts +2503 -0
- package/vendor/claude-bridge/src/log-paths.ts +19 -0
- package/vendor/claude-bridge/src/mcp-server.ts +169 -0
- package/vendor/claude-bridge/src/models.ts +164 -0
- package/vendor/claude-bridge/src/prompt-capture.ts +420 -0
- package/vendor/claude-bridge/src/prompt-stream.ts +102 -0
- package/vendor/claude-bridge/src/query-state.ts +118 -0
- package/vendor/claude-bridge/src/rate-limit.ts +48 -0
- package/vendor/claude-bridge/src/session-cwd.ts +38 -0
- package/vendor/claude-bridge/src/session-verify.ts +38 -0
- package/vendor/claude-bridge/src/skills.ts +20 -0
- package/vendor/claude-bridge/src/transcript.ts +86 -0
- package/vendor/claude-bridge/src/usage.ts +53 -0
- package/vendor/lore-pi-extension/package.json +1 -1
- package/vendor/lore-pi-extension/tools.ts +8 -5
- package/dist/web/assets/index-C72gAwgN.css +0 -1
- package/dist/web/assets/index-Dyw5G8Pg.js +0 -230
package/shared/types.ts
CHANGED
|
@@ -126,6 +126,28 @@ export interface ModelThinkingView {
|
|
|
126
126
|
custom: boolean; // 存过自定义档位(可以恢复默认)
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
export interface ClaudeCodeSettings {
|
|
130
|
+
enabled: boolean;
|
|
131
|
+
plan: "pro" | "max";
|
|
132
|
+
executable: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface ClaudeCodeView {
|
|
136
|
+
settings: SettingsFile;
|
|
137
|
+
status: ClaudeCodeStatus;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 本机 Claude Code 状态,不包含凭据或程序原始输出。 */
|
|
141
|
+
export interface ClaudeCodeStatus {
|
|
142
|
+
found: boolean;
|
|
143
|
+
executable?: string;
|
|
144
|
+
version?: string;
|
|
145
|
+
loggedIn: boolean;
|
|
146
|
+
email?: string;
|
|
147
|
+
plan?: string;
|
|
148
|
+
error?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
129
151
|
/** GET /api/providers:已配的服务商、引擎自带的服务商目录、跟着一起回的整份设置(前端要新 revision)。 */
|
|
130
152
|
export interface ProvidersView {
|
|
131
153
|
providers: ProviderView[];
|
|
@@ -214,6 +236,8 @@ export interface Assistant {
|
|
|
214
236
|
};
|
|
215
237
|
/** 派出去的活能用的能力;缺省全开(读总是有) */
|
|
216
238
|
abilities?: { exec: boolean; write: boolean; image: boolean };
|
|
239
|
+
/** 知识库二期:绑定的用户知识库 id。缺省 = 没绑定:不自动查,也没有 search_knowledge */
|
|
240
|
+
knowledge?: string[];
|
|
217
241
|
createdAt: Iso;
|
|
218
242
|
splitFrom?: { from: string; at: Iso; conversations: number };
|
|
219
243
|
}
|
|
@@ -245,6 +269,9 @@ export interface RoutineRun {
|
|
|
245
269
|
jobId: string;
|
|
246
270
|
at: Iso;
|
|
247
271
|
status: JobStatus;
|
|
272
|
+
durationMs?: number; // 技能二期:这次用时
|
|
273
|
+
tools?: number; // 技能二期:工具调用次数
|
|
274
|
+
tokens?: number; // 技能二期:输入加输出 token
|
|
248
275
|
}
|
|
249
276
|
|
|
250
277
|
export interface Routine {
|
|
@@ -256,6 +283,7 @@ export interface Routine {
|
|
|
256
283
|
active: boolean;
|
|
257
284
|
createdAt: Iso;
|
|
258
285
|
runs: RoutineRun[]; // 最近 20 次,新的在前
|
|
286
|
+
skill?: string; // 绑定这个助手自己的技能(AssistantSkill.name):每次执行按它做
|
|
259
287
|
}
|
|
260
288
|
|
|
261
289
|
export interface Delegation {
|
|
@@ -264,7 +292,7 @@ export interface Delegation {
|
|
|
264
292
|
to: string;
|
|
265
293
|
text: string;
|
|
266
294
|
createdAt: Iso;
|
|
267
|
-
status: "running" | "waiting" | "done" | "stuck" | "interrupted" | "returned";
|
|
295
|
+
status: "running" | "waiting" | "done" | "stuck" | "interrupted" | "returned" | "takenOver";
|
|
268
296
|
jobIds: string[];
|
|
269
297
|
reply?: string;
|
|
270
298
|
returnedText?: string; // 退回时补充的用户要求
|
|
@@ -323,6 +351,8 @@ export interface JobStep {
|
|
|
323
351
|
durationMs?: number;
|
|
324
352
|
/** tool:还在跑 */
|
|
325
353
|
running?: boolean;
|
|
354
|
+
/** 你补的那句已经交给任务,但任务还在做当前这一步,做完才会读到 */
|
|
355
|
+
queued?: boolean;
|
|
326
356
|
}
|
|
327
357
|
|
|
328
358
|
/** GET /api/jobs/:id/transcript */
|
|
@@ -340,7 +370,7 @@ export interface JobsPage {
|
|
|
340
370
|
export interface Job {
|
|
341
371
|
id: string; // "t-" + 4 位以上
|
|
342
372
|
assistantId: string;
|
|
343
|
-
assistantName?: string; //
|
|
373
|
+
assistantName?: string; // 建任务时的助手名:助手归档后,任务详情里照样显示
|
|
344
374
|
title: string; // 如 "查页面卡顿"
|
|
345
375
|
instruction: string;
|
|
346
376
|
status: JobStatus;
|
|
@@ -351,7 +381,7 @@ export interface Job {
|
|
|
351
381
|
usage?: JobUsage; // 输入/输出 token 和花费
|
|
352
382
|
durationMs?: number; // 用时(结束时间 − 开始时间)
|
|
353
383
|
stats: { reads: number; commands: number; edits: number };
|
|
354
|
-
summary?: string; // report
|
|
384
|
+
summary?: string; // report 的完整结果
|
|
355
385
|
reportPath?: string; // 完整报告
|
|
356
386
|
files?: string[]; // 产出文件(绝对路径)
|
|
357
387
|
cwd?: string; // 工作目录(缺省为 HOB_HOME/jobs/<id>/work)
|
|
@@ -360,10 +390,26 @@ export interface Job {
|
|
|
360
390
|
modelReason?: string; // 换了模型的原因,如「需要看图,换成了 X」
|
|
361
391
|
inputs?: string[]; // 交给任务的附件(绝对路径)
|
|
362
392
|
routineId?: string; // 由定时触发
|
|
393
|
+
skillsUsed?: string[]; // 这次读过的本助手技能(AssistantSkill.name)
|
|
363
394
|
delegationId?: string; // 转交回合创建的任务
|
|
395
|
+
/** 续做前每一轮结束时的样子(第 0 轮起)。对话里前面几张任务卡照它显示,最后一张跟着任务实时变 */
|
|
396
|
+
rounds?: JobRound[];
|
|
364
397
|
activity: ActivityLine[]; // 最近 200 行
|
|
365
398
|
}
|
|
366
399
|
|
|
400
|
+
export interface JobRound {
|
|
401
|
+
summary?: string;
|
|
402
|
+
files?: string[];
|
|
403
|
+
usage?: JobUsage;
|
|
404
|
+
reportPath?: string;
|
|
405
|
+
followUp?: string;
|
|
406
|
+
status: JobStatus;
|
|
407
|
+
statusReason?: string;
|
|
408
|
+
startedAt?: Iso;
|
|
409
|
+
endedAt?: Iso;
|
|
410
|
+
durationMs?: number;
|
|
411
|
+
}
|
|
412
|
+
|
|
367
413
|
export interface ApprovalOption {
|
|
368
414
|
key: "A" | "B" | "C";
|
|
369
415
|
label: string; // 如 "以后同类内容直接发布,无需确认"
|
|
@@ -406,16 +452,17 @@ export interface Draft {
|
|
|
406
452
|
createdAt: Iso;
|
|
407
453
|
}
|
|
408
454
|
|
|
409
|
-
export type NoteIcon = "memory" | "rule" | "routine" | "split" | "info";
|
|
455
|
+
export type NoteIcon = "memory" | "rule" | "routine" | "split" | "info" | "skill";
|
|
410
456
|
|
|
411
457
|
export type Item = (
|
|
412
458
|
| { kind: "user"; id: string; at: Iso; text: string; quote?: { itemId: string; text: string }; files?: string[]; from?: { assistantId: string; name: string; delegationId: string } }
|
|
413
|
-
| { kind: "assistant"; id: string; at: Iso; text: string; streaming?: boolean; routine?: { id: string; name: string } }
|
|
414
|
-
| { kind: "note"; id: string; at: Iso; icon: NoteIcon; text: string; handbackId?: string } // 居中灰色一行,如 "记忆已更新 · 不看融资新闻"
|
|
459
|
+
| { kind: "assistant"; id: string; at: Iso; text: string; streaming?: boolean; routine?: { id: string; name: string; jobId?: string } }
|
|
460
|
+
| { kind: "note"; id: string; at: Iso; icon: NoteIcon; text: string; handbackId?: string; path?: string } // path:点了预览这个文件(如技能的 SKILL.md) // 居中灰色一行,如 "记忆已更新 · 不看融资新闻"
|
|
415
461
|
| { kind: "question"; id: string; at: Iso; approvalId: string }
|
|
416
|
-
| { kind: "job"; id: string; at: Iso; jobId: string }
|
|
462
|
+
| { kind: "job"; id: string; at: Iso; jobId: string; round?: number } // 第几轮(续做一次 +1),缺省是第 0 轮
|
|
417
463
|
| { kind: "delegation"; id: string; at: Iso; delegationId: string }
|
|
418
464
|
| { kind: "draft"; id: string; at: Iso; draftId: string }
|
|
465
|
+
| { kind: "image"; id: string; at: Iso; prompt: string; size?: string; startedAt: Iso; status: "pending" | "done" | "failed"; path?: string; error?: string }
|
|
419
466
|
| { kind: "file"; id: string; at: Iso; path: string; name: string; meta?: string } // meta 如 "3200 字 · 4 张图"
|
|
420
467
|
) & {
|
|
421
468
|
/** 这条开启新的一组,前端在它上面拉开间距:事件触发的新一轮回复,或任务直接发进对话的图片、提问卡 */
|
|
@@ -466,6 +513,7 @@ export interface MemoryConfig {
|
|
|
466
513
|
provider: MemoryProvider;
|
|
467
514
|
providerExplicit: boolean; // 用户在设置里明确选过(没选过就是按有没有配好 Lore 推出的)
|
|
468
515
|
lore: LoreConnectionInfo;
|
|
516
|
+
lorePlugin: string; // Hob 自带的 Lore 插件版本(Lore 正式版的版本号)
|
|
469
517
|
}
|
|
470
518
|
|
|
471
519
|
/** POST /api/memory/lore-test:测试失败也是 200,原因在这里。 */
|
|
@@ -506,13 +554,47 @@ export interface Settings {
|
|
|
506
554
|
models: ModelSettings;
|
|
507
555
|
image: ImageSettings;
|
|
508
556
|
search: SearchSettings;
|
|
557
|
+
/** 云桌面令牌保存在 secrets.json,不包含在设置中。 */
|
|
558
|
+
desktop?: { url: string; enabled: boolean; publicAssistants?: string[] };
|
|
559
|
+
claude?: ClaudeCodeSettings;
|
|
509
560
|
/** 停用的技能,键为来源和技能名。 */
|
|
510
561
|
skills?: { disabled: string[] };
|
|
511
562
|
work: WorkSettings;
|
|
512
563
|
push: PushSettings;
|
|
564
|
+
/** 知识库二期:向量模型(Key 在 secrets.json 的 embedding);缺省 = 关闭,只按关键词查 */
|
|
565
|
+
knowledge?: { embedding: EmbeddingSettings };
|
|
513
566
|
}
|
|
514
567
|
|
|
568
|
+
/** 设置 → 知识库:向量模型,照图像生成的做法(兼容接口的地址 + 模型,Key 单独存)。 */
|
|
569
|
+
export type EmbeddingSettings = { source: "off" } | { source: "openai"; baseUrl: string; model: string };
|
|
570
|
+
|
|
571
|
+
/** GET /api/knowledge/embedding:Key 配没配(永不回显)。 */
|
|
572
|
+
export interface EmbeddingKeyView {
|
|
573
|
+
keyConfigured: boolean;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** PUT /api/knowledge/embedding 的返回:整份设置和 Key 状态。 */
|
|
577
|
+
export interface EmbeddingSettingsSaved {
|
|
578
|
+
settings: SettingsFile;
|
|
579
|
+
keyConfigured: boolean;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** POST /api/knowledge/embedding/test:失败也是 200,原因在这里。 */
|
|
583
|
+
export type EmbeddingTestResult = { ok: true; dims: number } | { ok: false; error: string };
|
|
584
|
+
|
|
515
585
|
/** HOB_HOME/settings.json:界面可改的设置,写入走 revision 比对(CAS)。 */
|
|
586
|
+
export interface DesktopView {
|
|
587
|
+
revision: number;
|
|
588
|
+
url: string;
|
|
589
|
+
enabled: boolean;
|
|
590
|
+
deployed: boolean;
|
|
591
|
+
tokenConfigured: boolean;
|
|
592
|
+
publicAssistants: string[];
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export interface DesktopScreen { id: string; running: boolean; browserRunning: boolean; viewers: number; lastActiveAt: string | null }
|
|
596
|
+
export type DesktopTestResult = { ok: true; health: { version: string; display: { width: number; height: number }; screens: DesktopScreen[] } } | { ok: false; error: string };
|
|
597
|
+
|
|
516
598
|
export interface SettingsFile {
|
|
517
599
|
revision: number;
|
|
518
600
|
settings: Settings;
|
|
@@ -581,20 +663,18 @@ export interface FileMeta {
|
|
|
581
663
|
page?: boolean; // 展示页面目录中的渲染产物
|
|
582
664
|
}
|
|
583
665
|
|
|
584
|
-
/**
|
|
585
|
-
export
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
size: number;
|
|
590
|
-
modifiedAt: Iso;
|
|
591
|
-
kind: FileKind;
|
|
592
|
-
}
|
|
666
|
+
/** 工作目录中的一项,目录的 count 是其下一层可见项数。 */
|
|
667
|
+
export type JobFileEntry = { path: string; rel: string; name: string } & (
|
|
668
|
+
| { type: "file"; size: number; modifiedAt: Iso; kind: FileKind }
|
|
669
|
+
| { type: "directory"; count: number }
|
|
670
|
+
);
|
|
593
671
|
|
|
594
|
-
/**
|
|
672
|
+
/** 一层最多返回 200 项,total 为截断前的总数。 */
|
|
595
673
|
export interface JobFilesView {
|
|
596
|
-
files: JobFileEntry[];
|
|
597
|
-
|
|
674
|
+
files: JobFileEntry[];
|
|
675
|
+
total: number;
|
|
676
|
+
fileCount: number;
|
|
677
|
+
directoryCount: number;
|
|
598
678
|
}
|
|
599
679
|
|
|
600
680
|
export interface Bootstrap {
|
|
@@ -607,12 +687,15 @@ export interface Bootstrap {
|
|
|
607
687
|
}
|
|
608
688
|
|
|
609
689
|
export type HobEvent =
|
|
690
|
+
| { type: "desktop-control"; screen: string; taken: boolean }
|
|
691
|
+
| { type: "desktop-screens"; screens: DesktopScreen[] }
|
|
610
692
|
| ({ type: "page-live" } & PageLive)
|
|
611
693
|
| { type: "assistant"; assistant: AssistantSummary } // 新建或变化
|
|
612
694
|
| { type: "assistant_removed"; id: string }
|
|
613
695
|
| { type: "item"; assistantId: string; item: Item } // 新条目,或按 id 替换
|
|
614
696
|
| { type: "delta"; assistantId: string; itemId: string; text: string } // 流式追加到 assistant 条目
|
|
615
697
|
| { type: "job"; job: Job }
|
|
698
|
+
| { type: "sediment-finished"; jobId: string }
|
|
616
699
|
| { type: "delegation"; delegation: Delegation }
|
|
617
700
|
| { type: "job_step"; jobId: string; step: JobStep } // 新的一步,或按 id 替换(输出流式更新、跑完补结果)
|
|
618
701
|
| { type: "router-activity"; assistantId: string; activity: RouterActivity | null }
|
|
@@ -621,7 +704,9 @@ export type HobEvent =
|
|
|
621
704
|
| { type: "draft"; draft: Draft }
|
|
622
705
|
| { type: "routine"; routine: Routine }
|
|
623
706
|
| { type: "routine_removed"; id: string }
|
|
624
|
-
| { type: "profile"; assistantId: string }
|
|
707
|
+
| { type: "profile"; assistantId: string } // rules/memories/worklog 变了,前端重拉 profile
|
|
708
|
+
| { type: "knowledge" } // 知识库有变化,打开着的知识库页面重拉
|
|
709
|
+
| { type: "skills"; assistantId: string }; // 这个助手的技能有变化,打开着的技能列表重拉
|
|
625
710
|
|
|
626
711
|
/** POST /api/assistants/:id/messages 的请求体 */
|
|
627
712
|
export interface SendMessageBody {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Hob 补丁
|
|
2
|
+
|
|
3
|
+
来源:`pi-claude-bridge@0.9.3`,MIT,原许可证见 `LICENSE`。
|
|
4
|
+
|
|
5
|
+
- 只读取 Hob 引擎目录的 `claude-bridge.json`,不合并任务工作目录的配置。
|
|
6
|
+
- Claude Code 普通请求和摘要请求都传 `--safe-mode`;摘要请求不再传空的 `settingSources`,因为当前 Claude Code 登录在该参数下不可用。
|
|
7
|
+
- 不通过全局环境设置安全模式,避免影响用户在任务中启动的 Claude Code。
|
|
8
|
+
- Hob 在模型目录初始化时预注册模型,所有会话复用同一个扩展模块;生命周期清理仅影响当前会话,避免并行对话和任务互相清除状态。
|
|
9
|
+
- 不修改 Hob 进程的 Claude Code 环境变量;启用桥调试日志时记录实际追加的系统提示词,供隔离验收。
|
|
10
|
+
- 移除桥直接读取、重建和删除 Claude Code 会话文件的路径。无法直接续接时,以 SDK 消息重放 Hob 自己的历史(包含图片);正常续接仍由 Claude Code 处理。这可能增加重启或上下文改写后的 token 用量。
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eli Dickinson
|
|
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.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-claude-bridge",
|
|
3
|
+
"version": "0.9.3",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Pi extension that uses Claude Code (via Agent SDK) as a model provider and adds an AskClaude tool.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi",
|
|
9
|
+
"extension",
|
|
10
|
+
"claude-code",
|
|
11
|
+
"claude-agent-sdk"
|
|
12
|
+
],
|
|
13
|
+
"author": "Eli Dickinson",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/elidickinson/pi-claude-bridge.git"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"src",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"assets"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test:unit": "node --import tsx --import ./tests/lib/setup.mjs --test tests/unit-*.mjs",
|
|
30
|
+
"test": "set -a && [ -f .env.test ] && . .env.test; set +a; CLAUDE_BRIDGE_TEST_LOG_DIR=$(mktemp -d); export CLAUDE_BRIDGE_TEST_LOG_DIR; npm run test:unit && tests/int-smoke.sh && tests/int-multi-turn.sh && tests/int-cache.sh && node --import tsx --test tests/int-*.mjs && node tests/lib/check-deadlock-logs.mjs \"$CLAUDE_BRIDGE_TEST_LOG_DIR\"",
|
|
31
|
+
"test:usage": "tests/usage-test.sh",
|
|
32
|
+
"typecheck": "tsc --noEmit"
|
|
33
|
+
},
|
|
34
|
+
"type": "module",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.293",
|
|
37
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
38
|
+
"cc-session-io": "^0.4.0",
|
|
39
|
+
"change-case": "^5.4.4"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@earendil-works/pi-ai": ">=0.86.1",
|
|
43
|
+
"@earendil-works/pi-coding-agent": ">=0.86.1",
|
|
44
|
+
"@earendil-works/pi-tui": ">=0.86.1",
|
|
45
|
+
"typebox": "*"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@anthropic-ai/sdk": "^0.124.0",
|
|
49
|
+
"@earendil-works/pi-ai": "^1.2.0",
|
|
50
|
+
"@earendil-works/pi-coding-agent": "^1.2.0",
|
|
51
|
+
"@earendil-works/pi-tui": "^1.2.0",
|
|
52
|
+
"@types/node": "^24.13.2",
|
|
53
|
+
"tsx": "^4.22.4",
|
|
54
|
+
"typebox": "^1.3.7",
|
|
55
|
+
"typescript": "^6.0.3"
|
|
56
|
+
},
|
|
57
|
+
"pi": {
|
|
58
|
+
"extensions": [
|
|
59
|
+
"./src/index.ts"
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Pi owns context-file discovery; the bridge only formats the list Pi loaded so
|
|
2
|
+
// Claude receives the same instructions, in the same order, that Pi applies.
|
|
3
|
+
|
|
4
|
+
type ContextFile = { path: string; content: string };
|
|
5
|
+
|
|
6
|
+
export function formatProjectContext(contextFiles: ContextFile[]): string | undefined {
|
|
7
|
+
if (contextFiles.length === 0) return undefined;
|
|
8
|
+
|
|
9
|
+
let prompt = "<project_context>\n\nProject-specific instructions and guidelines:\n\n";
|
|
10
|
+
for (const { path, content } of contextFiles) {
|
|
11
|
+
prompt += `<project_instructions path="${path}">\n${content}\n</project_instructions>\n\n`;
|
|
12
|
+
}
|
|
13
|
+
return `${prompt}</project_context>`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import type { Config } from "./config.js";
|
|
4
|
+
|
|
5
|
+
export type AskClaudeMode = "full" | "read" | "none";
|
|
6
|
+
|
|
7
|
+
export interface AskClaudeDefaults {
|
|
8
|
+
mode: AskClaudeMode;
|
|
9
|
+
isolated: boolean;
|
|
10
|
+
allowFull: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const PACKAGE_DEFAULT_MODE: AskClaudeMode = "read";
|
|
14
|
+
const PACKAGE_DEFAULT_ISOLATED = false;
|
|
15
|
+
|
|
16
|
+
export function resolveAskClaudeDefaults(conf: Config["askClaude"]): AskClaudeDefaults {
|
|
17
|
+
const rawAllowFull: unknown = conf?.allowFullMode;
|
|
18
|
+
const allowFull = rawAllowFull == null ? true : rawAllowFull === true;
|
|
19
|
+
const configuredMode: unknown = conf?.defaultMode;
|
|
20
|
+
const mode = configuredMode == null
|
|
21
|
+
? PACKAGE_DEFAULT_MODE
|
|
22
|
+
: configuredMode === "full" || configuredMode === "read" || configuredMode === "none"
|
|
23
|
+
? configuredMode
|
|
24
|
+
// Unrecognized value: the most restrictive mode, not the package default.
|
|
25
|
+
: "none";
|
|
26
|
+
return { mode: !allowFull && mode === "full" ? PACKAGE_DEFAULT_MODE : mode, isolated: conf?.defaultIsolated ?? PACKAGE_DEFAULT_ISOLATED, allowFull };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveAskClaudeMode(mode: unknown, defaults: AskClaudeDefaults): AskClaudeMode {
|
|
30
|
+
const resolved = mode === undefined ? defaults.mode : mode;
|
|
31
|
+
if (resolved !== "full" && resolved !== "read" && resolved !== "none") throw new Error(`Invalid AskClaude mode: ${String(resolved)}`);
|
|
32
|
+
if (resolved === "full" && !defaults.allowFull) throw new Error("AskClaude full mode is disabled.");
|
|
33
|
+
return resolved;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function modeDescription(defaults: AskClaudeDefaults): string {
|
|
37
|
+
const mark = (mode: AskClaudeMode) => (mode === defaults.mode ? " (default)" : "");
|
|
38
|
+
const parts = [
|
|
39
|
+
`"read"${mark("read")}: questions about the codebase — review, analysis, explain.`,
|
|
40
|
+
`"none"${mark("none")}: general knowledge only (no file access).`,
|
|
41
|
+
];
|
|
42
|
+
if (defaults.allowFull) {
|
|
43
|
+
parts.push(`"full"${mark("full")}: allows writing and bash execution (careful: runs without feedback to pi).`);
|
|
44
|
+
}
|
|
45
|
+
return parts.join(" ");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function buildAskClaudeParams(defaults: AskClaudeDefaults) {
|
|
49
|
+
const visibility = defaults.isolated
|
|
50
|
+
? "By default Claude sees only this prompt (isolated session)."
|
|
51
|
+
: "By default Claude sees the full conversation history.";
|
|
52
|
+
const trueDefault = defaults.isolated ? " (default)" : "";
|
|
53
|
+
const falseDefault = defaults.isolated ? "" : " (default)";
|
|
54
|
+
const modeValues: readonly AskClaudeMode[] = defaults.allowFull ? ["read", "full", "none"] : ["read", "none"];
|
|
55
|
+
return Type.Object({
|
|
56
|
+
prompt: Type.String({ description: `The question or task for Claude Code. ${visibility} Don't research up front, let Claude explore.` }),
|
|
57
|
+
mode: Type.Optional(StringEnum(modeValues, { description: modeDescription(defaults) })),
|
|
58
|
+
model: Type.Optional(Type.String({ description: 'Claude model (e.g. "opus", "sonnet", "haiku", or full ID). Defaults to "opus".' })),
|
|
59
|
+
thinking: Type.Optional(StringEnum(["off", "minimal", "low", "medium", "high", "xhigh"] as const, { description: "Thinking effort level. Omit to use Claude Code's default." })),
|
|
60
|
+
isolated: Type.Optional(Type.Boolean({ description: `When true${trueDefault}, Claude sees only this prompt (clean session). When false${falseDefault}, Claude sees the full conversation history.` })),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function askClaudeToolDescription(defaults: AskClaudeDefaults, override?: string | null): string {
|
|
65
|
+
if (override != null) return override;
|
|
66
|
+
const suffix = " Prefer to handle straightforward tasks yourself.";
|
|
67
|
+
if (!defaults.allowFull) {
|
|
68
|
+
const middle = defaults.mode === "none"
|
|
69
|
+
? 'Defaults to no file access — pass mode "read" to let Claude Code explore the codebase; it can never make changes.'
|
|
70
|
+
: "Read-only — Claude Code can explore the codebase but not make changes.";
|
|
71
|
+
return `Delegate to Claude Code for a second opinion or analysis (code review, architecture questions, debugging theories). ${middle}${suffix}`;
|
|
72
|
+
}
|
|
73
|
+
const middle = defaults.mode === "full"
|
|
74
|
+
? 'Defaults to full mode — Claude Code writes files and runs bash without feedback to pi; pass mode "read" to keep it to exploration.'
|
|
75
|
+
: defaults.mode === "none"
|
|
76
|
+
? 'Defaults to no file access — pass mode "read" to let Claude Code explore the codebase, or "full" for a task that requires changes.'
|
|
77
|
+
: "Defaults to read-only mode — use full mode when the user wants to delegate a task that requires changes.";
|
|
78
|
+
return `Delegate to Claude Code for a second opinion or analysis (code review, architecture questions, debugging theories), or to autonomously handle a task. ${middle}${suffix}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function askClaudeCallTags(
|
|
82
|
+
args: { mode?: AskClaudeMode; model?: string; thinking?: string; isolated?: boolean },
|
|
83
|
+
defaults: AskClaudeDefaults,
|
|
84
|
+
): string[] {
|
|
85
|
+
const tags: string[] = [];
|
|
86
|
+
const mode = args.mode ?? defaults.mode;
|
|
87
|
+
if (mode !== PACKAGE_DEFAULT_MODE || (args.mode !== undefined && args.mode !== defaults.mode)) tags.push(`mode=${mode}`);
|
|
88
|
+
if (args.model) tags.push(`model=${args.model}`);
|
|
89
|
+
if (args.thinking) tags.push(`thinking=${args.thinking}`);
|
|
90
|
+
if ((args.isolated ?? defaults.isolated) !== PACKAGE_DEFAULT_ISOLATED) tags.push("isolated");
|
|
91
|
+
return tags;
|
|
92
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Status-line rendering helpers for the AskClaude tool.
|
|
2
|
+
//
|
|
3
|
+
// While Claude Code runs inside an AskClaude call, the pi TUI can't surface
|
|
4
|
+
// each tool_use individually — there's only one status row for the whole
|
|
5
|
+
// delegation. These helpers shape a tool_use record into a short, path-aware
|
|
6
|
+
// label (e.g. "Read(src/foo.ts)", "Bash(git log --oneline…)") and collapse
|
|
7
|
+
// runs of the same tool so the line doesn't flicker. Used only by
|
|
8
|
+
// promptAndWait; the provider path exposes tools directly through pi's TUI
|
|
9
|
+
// and doesn't need this.
|
|
10
|
+
|
|
11
|
+
export interface ToolCallState {
|
|
12
|
+
name: string;
|
|
13
|
+
status: string;
|
|
14
|
+
rawInput?: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function extractPath(rawInput: unknown): string | undefined {
|
|
18
|
+
if (!rawInput || typeof rawInput !== "object") return undefined;
|
|
19
|
+
const input = rawInput as Record<string, unknown>;
|
|
20
|
+
if (typeof input.file_path === "string") return input.file_path;
|
|
21
|
+
if (typeof input.path === "string") return input.path;
|
|
22
|
+
if (typeof input.command === "string") return input.command.substring(0, 80);
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function shortPath(p: string): string {
|
|
27
|
+
const cwd = process.cwd();
|
|
28
|
+
if (p.startsWith(cwd + "/")) return p.slice(cwd.length + 1);
|
|
29
|
+
if (p.startsWith("/")) {
|
|
30
|
+
const parts = p.split("/");
|
|
31
|
+
if (parts.length > 3) return parts.slice(-2).join("/");
|
|
32
|
+
}
|
|
33
|
+
return p;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function formatToolAction(tc: ToolCallState): string | undefined {
|
|
37
|
+
const path = extractPath(tc.rawInput);
|
|
38
|
+
const verb = tc.name.toLowerCase().split(/\s/)[0];
|
|
39
|
+
if (verb === "read" || verb === "readfile") {
|
|
40
|
+
return path ? `Read(${shortPath(path)})` : "Read";
|
|
41
|
+
} else if (verb === "glob") {
|
|
42
|
+
const input = tc.rawInput as Record<string, unknown> | undefined;
|
|
43
|
+
const pat = typeof input?.pattern === "string" ? input.pattern.slice(0, 40) : "";
|
|
44
|
+
return pat ? `Glob(${pat})` : "Glob";
|
|
45
|
+
} else if (verb === "edit" || verb === "write" || verb === "writefile" || verb === "multiedit") {
|
|
46
|
+
return path ? `Edit(${shortPath(path)})` : "Edit";
|
|
47
|
+
} else if (verb === "bashoutput") {
|
|
48
|
+
return undefined; // redundant with preceding Bash call
|
|
49
|
+
} else if (verb === "bash" || verb === "terminal") {
|
|
50
|
+
return path ? `Bash(${path})` : "Bash";
|
|
51
|
+
} else if (verb === "agent") {
|
|
52
|
+
const input = tc.rawInput as Record<string, unknown> | undefined;
|
|
53
|
+
return `Agent(${String(input?.description ?? "").slice(0, 40)})`;
|
|
54
|
+
} else if (verb === "grep") {
|
|
55
|
+
const input = tc.rawInput as Record<string, unknown> | undefined;
|
|
56
|
+
const pat = typeof input?.pattern === "string" ? input.pattern.slice(0, 40) : "";
|
|
57
|
+
return pat ? `Grep(${pat})` : "Grep";
|
|
58
|
+
} else if (verb === "skill") {
|
|
59
|
+
const input = tc.rawInput as Record<string, unknown> | undefined;
|
|
60
|
+
const name = typeof input?.skill === "string" ? input.skill.slice(0, 40) : "";
|
|
61
|
+
return name ? `Skill(${name})` : "Skill";
|
|
62
|
+
} else if (verb === "todowrite" || verb === "taskcreate" || verb === "taskupdate") {
|
|
63
|
+
const todos = Array.isArray((tc.rawInput as any)?.todos) ? (tc.rawInput as any).todos : [];
|
|
64
|
+
const current = todos.find((t: any) => t.status === "in_progress") ?? todos.find((t: any) => t.status === "pending");
|
|
65
|
+
const label = current ? String(current.content ?? "").slice(0, 40) : "";
|
|
66
|
+
return label || undefined;
|
|
67
|
+
} else if (verb === "askclaude") {
|
|
68
|
+
// Recursive — don't show AskClaude in its own action summary
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
return tc.name;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function buildActionSummary(calls: Map<string, ToolCallState>): string {
|
|
75
|
+
const parts: string[] = [];
|
|
76
|
+
let prevVerb = "";
|
|
77
|
+
for (const [, tc] of calls) {
|
|
78
|
+
const action = formatToolAction(tc);
|
|
79
|
+
if (!action) continue;
|
|
80
|
+
const verb = tc.name.toLowerCase().split(/\s/)[0];
|
|
81
|
+
// Collapse consecutive calls to the same tool — keep only the latest
|
|
82
|
+
if (verb === prevVerb) {
|
|
83
|
+
parts[parts.length - 1] = action;
|
|
84
|
+
} else {
|
|
85
|
+
parts.push(action);
|
|
86
|
+
}
|
|
87
|
+
prevVerb = verb;
|
|
88
|
+
}
|
|
89
|
+
return parts.join("; ");
|
|
90
|
+
}
|