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.
Files changed (86) hide show
  1. package/README.md +75 -5
  2. package/README.zh-CN.md +75 -5
  3. package/dist/web/assets/DesktopView-rVxacveF.js +5 -0
  4. package/dist/web/assets/index-B-8ISOr1.js +267 -0
  5. package/dist/web/assets/index-Cu1Pf8yX.css +1 -0
  6. package/dist/web/assets/{math-zZ4JA8eM.js → math-weKGDUmL.js} +2 -2
  7. package/dist/web/index.html +2 -2
  8. package/dist/web/mock/desktop.jpg +0 -0
  9. package/dist/web/mock/photo.jpg +0 -0
  10. package/dist/web/mock/photo2.jpg +0 -0
  11. package/dist/web/mock/sunset.html +90 -0
  12. 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
  13. package/package.json +10 -2
  14. package/server/src/actions.ts +24 -57
  15. package/server/src/assistant-skills.ts +201 -0
  16. package/server/src/boot.ts +1 -0
  17. package/server/src/claude-status.ts +47 -0
  18. package/server/src/config.ts +3 -1
  19. package/server/src/delegations.ts +40 -12
  20. package/server/src/desktop-control.ts +152 -0
  21. package/server/src/desktop-events.ts +63 -0
  22. package/server/src/desktop-public.ts +43 -0
  23. package/server/src/desktop-tools.ts +161 -0
  24. package/server/src/desktop.ts +76 -0
  25. package/server/src/embedding.ts +52 -0
  26. package/server/src/files.ts +63 -43
  27. package/server/src/http.ts +334 -15
  28. package/server/src/jobs.ts +330 -59
  29. package/server/src/knowledge-index.ts +123 -0
  30. package/server/src/knowledge-tools.ts +94 -0
  31. package/server/src/knowledge.ts +451 -0
  32. package/server/src/lore-entry.ts +10 -0
  33. package/server/src/main.ts +6 -0
  34. package/server/src/mcp.ts +745 -0
  35. package/server/src/memory.ts +11 -4
  36. package/server/src/now.ts +33 -0
  37. package/server/src/overview.ts +1 -1
  38. package/server/src/pages.ts +1 -1
  39. package/server/src/pi.ts +92 -16
  40. package/server/src/prompt.ts +46 -154
  41. package/server/src/providers.ts +3 -2
  42. package/server/src/push.ts +42 -7
  43. package/server/src/router-activity.ts +4 -0
  44. package/server/src/router-tools.ts +132 -38
  45. package/server/src/router.ts +159 -19
  46. package/server/src/routines.ts +14 -4
  47. package/server/src/runtime.ts +15 -0
  48. package/server/src/settings.ts +43 -1
  49. package/server/src/store.ts +4 -3
  50. package/server/src/thumbs.ts +49 -0
  51. package/server/src/transcript.ts +57 -4
  52. package/server/src/worker-tools.ts +14 -12
  53. package/shared/knowledge.ts +92 -0
  54. package/shared/mcp.ts +91 -0
  55. package/shared/media.ts +6 -0
  56. package/shared/plain-text.ts +31 -0
  57. package/shared/skills.ts +18 -0
  58. package/shared/sprites.ts +198 -0
  59. package/shared/types.ts +105 -20
  60. package/vendor/claude-bridge/HOB-PATCH.md +10 -0
  61. package/vendor/claude-bridge/LICENSE +21 -0
  62. package/vendor/claude-bridge/package.json +62 -0
  63. package/vendor/claude-bridge/src/agents-md.ts +14 -0
  64. package/vendor/claude-bridge/src/askclaude-schema.ts +92 -0
  65. package/vendor/claude-bridge/src/askclaude-ui.ts +90 -0
  66. package/vendor/claude-bridge/src/attachments.ts +135 -0
  67. package/vendor/claude-bridge/src/config.ts +105 -0
  68. package/vendor/claude-bridge/src/convert.ts +222 -0
  69. package/vendor/claude-bridge/src/extract-tool-results.ts +47 -0
  70. package/vendor/claude-bridge/src/index.ts +2503 -0
  71. package/vendor/claude-bridge/src/log-paths.ts +19 -0
  72. package/vendor/claude-bridge/src/mcp-server.ts +169 -0
  73. package/vendor/claude-bridge/src/models.ts +164 -0
  74. package/vendor/claude-bridge/src/prompt-capture.ts +420 -0
  75. package/vendor/claude-bridge/src/prompt-stream.ts +102 -0
  76. package/vendor/claude-bridge/src/query-state.ts +118 -0
  77. package/vendor/claude-bridge/src/rate-limit.ts +48 -0
  78. package/vendor/claude-bridge/src/session-cwd.ts +38 -0
  79. package/vendor/claude-bridge/src/session-verify.ts +38 -0
  80. package/vendor/claude-bridge/src/skills.ts +20 -0
  81. package/vendor/claude-bridge/src/transcript.ts +86 -0
  82. package/vendor/claude-bridge/src/usage.ts +53 -0
  83. package/vendor/lore-pi-extension/package.json +1 -1
  84. package/vendor/lore-pi-extension/tools.ts +8 -5
  85. package/dist/web/assets/index-C72gAwgN.css +0 -1
  86. 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; // 建任务时的助手名:助手并回 Hob 后,任务详情里照样显示
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 的摘要(≤600 字)
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
- /** GET /api/jobs/:id/files:任务工作目录里的一个文件。 */
585
- export interface JobFileEntry {
586
- path: string; // 绝对路径
587
- rel: string; // 相对任务工作目录,如 "src/index.ts"
588
- name: string;
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
- /** GET /api/jobs/:id/files:任务工作目录(HOB_HOME/jobs/<id>/work)里的文件,最多 200 个。 */
672
+ /** 一层最多返回 200 项,total 为截断前的总数。 */
595
673
  export interface JobFilesView {
596
- files: JobFileEntry[]; // 修改时间倒序
597
- hasMore: boolean;
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 }; // rules/memories/worklog 变了,前端重拉 profile
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
+ }