@hienlh/ppm 0.9.80 → 0.9.82

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 (162) hide show
  1. package/.opencode/.env.example +98 -0
  2. package/.opencode/skills/ads-management/scripts/.env.example +13 -0
  3. package/.opencode/skills/ai-multimodal/.env.example +230 -0
  4. package/.opencode/skills/cip-design/.env.example +6 -0
  5. package/.opencode/skills/devops/.env.example +76 -0
  6. package/.opencode/skills/docs-seeker/.env.example +15 -0
  7. package/.opencode/skills/elevenlabs/.env.example +3 -0
  8. package/.opencode/skills/marketing-dashboard/.env.example +15 -0
  9. package/.opencode/skills/marketing-dashboard/app/.env.example +2 -0
  10. package/.opencode/skills/marketing-dashboard/server/.env.example +2 -0
  11. package/.opencode/skills/mcp-management/scripts/dist/analyze-tools.js +70 -0
  12. package/.opencode/skills/mcp-management/scripts/dist/cli.js +160 -0
  13. package/.opencode/skills/mcp-management/scripts/dist/mcp-client.js +183 -0
  14. package/.opencode/skills/payment-integration/scripts/.env.example +20 -0
  15. package/.opencode/skills/sequential-thinking/.env.example +8 -0
  16. package/.repomixignore +22 -0
  17. package/AGENTS.md +62 -0
  18. package/CHANGELOG.md +17 -0
  19. package/CLAUDE.md +12 -0
  20. package/assets/skills/ppm-guide/SKILL.md +61 -0
  21. package/bun.lock +9 -1
  22. package/dist/web/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  23. package/dist/web/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  24. package/dist/web/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  25. package/dist/web/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  26. package/dist/web/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  27. package/dist/web/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  28. package/dist/web/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  29. package/dist/web/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  30. package/dist/web/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  31. package/dist/web/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  32. package/dist/web/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  33. package/dist/web/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  34. package/dist/web/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  35. package/dist/web/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  36. package/dist/web/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  37. package/dist/web/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  38. package/dist/web/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  39. package/dist/web/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  40. package/dist/web/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  41. package/dist/web/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  42. package/dist/web/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  43. package/dist/web/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  44. package/dist/web/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  45. package/dist/web/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  46. package/dist/web/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  47. package/dist/web/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  48. package/dist/web/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  49. package/dist/web/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  50. package/dist/web/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  51. package/dist/web/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  52. package/dist/web/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  53. package/dist/web/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  54. package/dist/web/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  55. package/dist/web/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  56. package/dist/web/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  57. package/dist/web/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  58. package/dist/web/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  59. package/dist/web/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  60. package/dist/web/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  61. package/dist/web/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  62. package/dist/web/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  63. package/dist/web/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  64. package/dist/web/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  65. package/dist/web/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  66. package/dist/web/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  67. package/dist/web/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  68. package/dist/web/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  69. package/dist/web/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  70. package/dist/web/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  71. package/dist/web/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  72. package/dist/web/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  73. package/dist/web/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  74. package/dist/web/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  75. package/dist/web/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  76. package/dist/web/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  77. package/dist/web/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  78. package/dist/web/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  79. package/dist/web/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  80. package/dist/web/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  81. package/dist/web/assets/chat-tab-bS86TsT5.js +10 -0
  82. package/dist/web/assets/{code-editor-BFe-hnpF.js → code-editor-BaNaQ33b.js} +1 -1
  83. package/dist/web/assets/{database-viewer-BeY2V5QI.js → database-viewer-C5MVw8cJ.js} +1 -1
  84. package/dist/web/assets/{diff-viewer-D6xzs8PP.js → diff-viewer-CUbFMWVo.js} +1 -1
  85. package/dist/web/assets/{extension-webview-Cd1XYFXO.js → extension-webview-CwGufYEP.js} +1 -1
  86. package/dist/web/assets/{git-graph-D2XXpiMQ.js → git-graph-BD7A7MLo.js} +1 -1
  87. package/dist/web/assets/index-BYXjCNlK.css +2 -0
  88. package/dist/web/assets/index-CpzkPHOC.js +30 -0
  89. package/dist/web/assets/keybindings-store-DsaANvBz.js +1 -0
  90. package/dist/web/assets/markdown-renderer-C19IsITh.js +326 -0
  91. package/dist/web/assets/{port-forwarding-tab-B5rj_I66.js → port-forwarding-tab-BF79F1iL.js} +1 -1
  92. package/dist/web/assets/{postgres-viewer-DnlqzOnm.js → postgres-viewer-_nYiO_wp.js} +1 -1
  93. package/dist/web/assets/{settings-tab-CNZpuPD3.js → settings-tab-C1SQMbSu.js} +1 -1
  94. package/dist/web/assets/{sql-query-editor-Df2kzbPj.js → sql-query-editor-6OFvxxuN.js} +1 -1
  95. package/dist/web/assets/{sqlite-viewer-Cj1G70z4.js → sqlite-viewer-SNVYFXvB.js} +1 -1
  96. package/dist/web/assets/{terminal-tab-Dv9A7Xe2.js → terminal-tab-BJEkmrDt.js} +1 -1
  97. package/dist/web/assets/{use-monaco-theme-CPfIEo8t.js → use-monaco-theme-r8FzlCWr.js} +1 -1
  98. package/dist/web/index.html +2 -2
  99. package/dist/web/sw.js +1 -1
  100. package/docs/codebase-summary.md +78 -0
  101. package/docs/project-changelog.md +29 -0
  102. package/docs/system-architecture.md +2 -0
  103. package/package.json +5 -2
  104. package/release-manifest.json +15784 -0
  105. package/scripts/check-ppm-dir-usage.sh +21 -0
  106. package/scripts/generate-ppm-guide.ts +92 -0
  107. package/src/cli/commands/init.ts +2 -1
  108. package/src/cli/commands/logs.ts +11 -11
  109. package/src/cli/commands/report.ts +3 -2
  110. package/src/cli/commands/restart.ts +22 -23
  111. package/src/cli/commands/skills-cmd.ts +123 -0
  112. package/src/cli/commands/status.ts +7 -8
  113. package/src/cli/commands/stop.ts +18 -19
  114. package/src/index.ts +3 -0
  115. package/src/lib/account-crypto.ts +12 -7
  116. package/src/providers/claude-agent-sdk.ts +42 -11
  117. package/src/server/index.ts +8 -8
  118. package/src/server/routes/chat.ts +4 -2
  119. package/src/server/routes/upgrade.ts +3 -5
  120. package/src/server/ws/chat.ts +31 -0
  121. package/src/services/cloud-ws.service.ts +6 -3
  122. package/src/services/cloud.service.ts +20 -19
  123. package/src/services/cloudflared.service.ts +13 -13
  124. package/src/services/config.service.ts +5 -7
  125. package/src/services/db.service.ts +5 -6
  126. package/src/services/extension-rpc-handlers.ts +2 -2
  127. package/src/services/extension.service.ts +9 -12
  128. package/src/services/ppm-dir.ts +14 -0
  129. package/src/services/slash-discovery/builtin-commands.ts +53 -0
  130. package/src/services/slash-discovery/builtin-handlers.ts +65 -0
  131. package/src/services/slash-discovery/definition-source.ts +27 -0
  132. package/src/services/slash-discovery/discover-skill-roots.ts +128 -0
  133. package/src/services/slash-discovery/fuzzy-search.ts +76 -0
  134. package/src/services/slash-discovery/index.ts +42 -0
  135. package/src/services/slash-discovery/resolve-overrides.ts +41 -0
  136. package/src/services/slash-discovery/skill-loader.ts +156 -0
  137. package/src/services/slash-discovery/types.ts +51 -0
  138. package/src/services/slash-items.service.ts +4 -182
  139. package/src/services/supervisor-state.ts +14 -15
  140. package/src/services/supervisor-stopped-page.ts +2 -4
  141. package/src/services/supervisor.ts +15 -15
  142. package/src/services/tunnel.service.ts +22 -5
  143. package/src/services/upgrade.service.ts +2 -3
  144. package/src/types/chat.ts +3 -1
  145. package/src/web/components/chat/chat-history-bar.tsx +2 -15
  146. package/src/web/components/chat/chat-tab.tsx +5 -2
  147. package/src/web/components/chat/message-input.tsx +48 -6
  148. package/src/web/components/chat/message-list.tsx +19 -5
  149. package/src/web/components/chat/slash-command-picker.tsx +21 -12
  150. package/src/web/components/layout/mobile-nav.tsx +47 -21
  151. package/src/web/components/layout/panel-layout.tsx +11 -0
  152. package/src/web/components/layout/upgrade-banner.tsx +48 -2
  153. package/src/web/components/shared/markdown-renderer.tsx +5 -2
  154. package/src/web/hooks/use-chat.ts +33 -1
  155. package/src/web/main.tsx +1 -0
  156. package/src/web/stores/panel-store.ts +25 -1
  157. package/src/web/styles/globals.css +14 -0
  158. package/dist/web/assets/chat-tab-CmSLt4tg.js +0 -10
  159. package/dist/web/assets/index-BtwsLrdT.css +0 -2
  160. package/dist/web/assets/index-D6_wwsL_.js +0 -30
  161. package/dist/web/assets/keybindings-store-C8ryKudw.js +0 -1
  162. package/dist/web/assets/markdown-renderer-xYMhd9cE.js +0 -69
@@ -1,184 +1,6 @@
1
- import { resolve, basename, relative, sep } from "node:path";
2
- import { homedir } from "node:os";
3
- import { readdirSync, readFileSync, existsSync, statSync } from "node:fs";
4
- import yaml from "js-yaml";
5
-
6
- export interface SlashItem {
7
- type: "skill" | "command";
8
- /** Slash name, e.g. "review", "devops/deploy", "ck:research" */
9
- name: string;
10
- description: string;
11
- argumentHint?: string;
12
- /** Where the item comes from */
13
- scope: "project" | "user";
14
- }
15
-
16
- /** Safely coerce a frontmatter value to string, returns undefined if not a scalar. */
17
- function str(val: unknown): string | undefined {
18
- if (typeof val === "string") return val;
19
- if (typeof val === "number" || typeof val === "boolean") return String(val);
20
- return undefined;
21
- }
22
-
23
1
  /**
24
- * Parse YAML frontmatter from a Markdown file.
2
+ * Thin re-export wrapper actual discovery logic lives in slash-discovery/.
3
+ * Kept for backward compatibility with existing imports.
25
4
  */
26
- function parseFrontmatter(content: string): { meta: Record<string, unknown>; body: string } {
27
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
28
- if (!match || !match[1]) return { meta: {}, body: content };
29
- try {
30
- const meta = yaml.load(match[1]) as Record<string, unknown>;
31
- const body = content.slice(match[0]!.length).trim();
32
- return { meta: meta ?? {}, body };
33
- } catch {
34
- return { meta: {}, body: content };
35
- }
36
- }
37
-
38
- /**
39
- * Recursively walk a directory tree, calling `visitor` for every file.
40
- * Ignores unreadable dirs/files silently.
41
- */
42
- function walkDir(dir: string, visitor: (filePath: string) => void): void {
43
- let entries: string[];
44
- try {
45
- entries = readdirSync(dir);
46
- } catch {
47
- return;
48
- }
49
- for (const entry of entries) {
50
- const full = resolve(dir, entry);
51
- try {
52
- const stat = statSync(full);
53
- if (stat.isDirectory()) {
54
- walkDir(full, visitor);
55
- } else if (stat.isFile()) {
56
- visitor(full);
57
- }
58
- } catch { /* skip */ }
59
- }
60
- }
61
-
62
- /**
63
- * Collect commands from a commands directory (recursive).
64
- * `commands/devops/deploy.md` → name `devops/deploy`
65
- */
66
- function collectCommands(commandsDir: string, scope: "project" | "user"): SlashItem[] {
67
- const items: SlashItem[] = [];
68
- if (!existsSync(commandsDir)) return items;
69
- walkDir(commandsDir, (filePath) => {
70
- if (!filePath.endsWith(".md")) return;
71
- try {
72
- const content = readFileSync(filePath, "utf-8");
73
- const { meta } = parseFrontmatter(content);
74
- const rel = relative(commandsDir, filePath);
75
- const name = rel.replace(/\.md$/, "").split(sep).join("/");
76
- items.push({
77
- type: "command",
78
- name: str(meta.name) ?? name,
79
- description: str(meta.description) ?? "",
80
- argumentHint: str(meta["argument-hint"]),
81
- scope,
82
- });
83
- } catch { /* skip */ }
84
- });
85
- return items;
86
- }
87
-
88
- /**
89
- * Collect skills from a skills directory.
90
- *
91
- * @param skillsDir Root skills directory to scan
92
- * @param scope "project" or "user"
93
- * @param strictMode When true, ONLY pick up SKILL.md files (used for user-global
94
- * which can have many supporting .md files per skill).
95
- * When false, also pick up loose .md files outside SKILL.md dirs
96
- * (used for project-local where flat layout is common).
97
- */
98
- function collectSkills(skillsDir: string, scope: "project" | "user", strictMode: boolean): SlashItem[] {
99
- const items: SlashItem[] = [];
100
- if (!existsSync(skillsDir)) return items;
101
-
102
- const dirsWithSkillMd = new Set<string>();
103
-
104
- // Pass 1: SKILL.md files (directory-based skills)
105
- walkDir(skillsDir, (filePath) => {
106
- if (basename(filePath) !== "SKILL.md") return;
107
- try {
108
- const content = readFileSync(filePath, "utf-8");
109
- const { meta } = parseFrontmatter(content);
110
- const skillDir = resolve(filePath, "..");
111
- dirsWithSkillMd.add(skillDir);
112
- const rel = relative(skillsDir, skillDir);
113
- const pathName = rel.split(sep).join("/");
114
- const name = str(meta.name) ?? pathName;
115
- if (!name) return;
116
- items.push({
117
- type: "skill",
118
- name,
119
- description: str(meta.description) ?? "",
120
- scope,
121
- });
122
- } catch { /* skip */ }
123
- });
124
-
125
- // Pass 2 (only in relaxed mode): loose .md files not inside a SKILL.md directory
126
- if (!strictMode) {
127
- walkDir(skillsDir, (filePath) => {
128
- if (!filePath.endsWith(".md")) return;
129
- if (basename(filePath) === "SKILL.md") return;
130
- const dir = resolve(filePath, "..");
131
- // Skip supporting files inside a skill dir (or any ancestor)
132
- let ancestor = dir;
133
- while (ancestor.startsWith(skillsDir) && ancestor !== skillsDir) {
134
- if (dirsWithSkillMd.has(ancestor)) return;
135
- ancestor = resolve(ancestor, "..");
136
- }
137
- try {
138
- const content = readFileSync(filePath, "utf-8");
139
- const { meta } = parseFrontmatter(content);
140
- const rel = relative(skillsDir, filePath);
141
- const pathName = rel.replace(/\.md$/, "").split(sep).join("/");
142
- const name = str(meta.name) ?? pathName;
143
- if (!name) return;
144
- items.push({
145
- type: "skill",
146
- name,
147
- description: str(meta.description) ?? "",
148
- scope,
149
- });
150
- } catch { /* skip */ }
151
- });
152
- }
153
-
154
- return items;
155
- }
156
-
157
- /**
158
- * Scan for available slash commands and skills.
159
- *
160
- * Sources (merged, project overrides user if same name):
161
- * 1. User-global: ~/.claude/commands/ and ~/.claude/skills/ (strict: SKILL.md only)
162
- * 2. Project-local: <projectPath>/.claude/commands/ and .claude/skills/ (relaxed: also loose .md)
163
- */
164
- export function listSlashItems(projectPath: string): SlashItem[] {
165
- const home = homedir();
166
- const globalClaude = resolve(home, ".claude");
167
-
168
- // Collect from both scopes (user-global uses strict mode)
169
- const userCommands = collectCommands(resolve(globalClaude, "commands"), "user");
170
- const userSkills = collectSkills(resolve(globalClaude, "skills"), "user", true);
171
- const projectCommands = collectCommands(resolve(projectPath, ".claude", "commands"), "project");
172
- const projectSkills = collectSkills(resolve(projectPath, ".claude", "skills"), "project", false);
173
-
174
- // Merge: project items override user items with the same name
175
- const map = new Map<string, SlashItem>();
176
- for (const item of [...userCommands, ...userSkills]) {
177
- map.set(`${item.type}:${item.name}`, item);
178
- }
179
- for (const item of [...projectCommands, ...projectSkills]) {
180
- map.set(`${item.type}:${item.name}`, item);
181
- }
182
-
183
- return Array.from(map.values());
184
- }
5
+ export { listSlashItems, searchSlashItems } from "./slash-discovery/index.ts";
6
+ export type { SlashItem } from "./slash-discovery/types.ts";
@@ -3,17 +3,16 @@
3
3
  * Extracted from supervisor.ts to keep the orchestrator lean.
4
4
  */
5
5
  import { resolve } from "node:path";
6
- import { homedir } from "node:os";
7
6
  import {
8
7
  readFileSync, writeFileSync, existsSync, unlinkSync, renameSync, openSync, closeSync,
9
8
  } from "node:fs";
10
9
  import { constants } from "node:fs";
10
+ import { getPpmDir } from "./ppm-dir.ts";
11
11
 
12
- const PPM_DIR = resolve(process.env.PPM_HOME || resolve(homedir(), ".ppm"));
13
- export const CMD_FILE = resolve(PPM_DIR, ".supervisor-cmd");
14
- export const STATUS_FILE = resolve(PPM_DIR, "status.json");
15
- export const PID_FILE = resolve(PPM_DIR, "ppm.pid");
16
- export const LOCK_FILE = resolve(PPM_DIR, ".start-lock");
12
+ export const CMD_FILE = () => resolve(getPpmDir(), ".supervisor-cmd");
13
+ export const STATUS_FILE = () => resolve(getPpmDir(), "status.json");
14
+ export const PID_FILE = () => resolve(getPpmDir(), "ppm.pid");
15
+ export const LOCK_FILE = () => resolve(getPpmDir(), ".start-lock");
17
16
 
18
17
  // ─── State ─────────────────────────────────────────────────────────────
19
18
  export type SupervisorState = "running" | "paused" | "stopped" | "upgrading";
@@ -47,7 +46,7 @@ function atomicWriteJson(filePath: string, data: unknown) {
47
46
 
48
47
  export function readStatus(): Record<string, unknown> {
49
48
  try {
50
- if (existsSync(STATUS_FILE)) return JSON.parse(readFileSync(STATUS_FILE, "utf-8"));
49
+ if (existsSync(STATUS_FILE())) return JSON.parse(readFileSync(STATUS_FILE(), "utf-8"));
51
50
  } catch {}
52
51
  return {};
53
52
  }
@@ -55,7 +54,7 @@ export function readStatus(): Record<string, unknown> {
55
54
  export function updateStatus(patch: Record<string, unknown>) {
56
55
  try {
57
56
  const data = { ...readStatus(), ...patch };
58
- atomicWriteJson(STATUS_FILE, data);
57
+ atomicWriteJson(STATUS_FILE(), data);
59
58
  } catch {}
60
59
  }
61
60
 
@@ -64,9 +63,9 @@ export type CmdAction = "soft_stop" | "resume";
64
63
 
65
64
  /** Atomically claim + read command file (rename to .claimed, read, delete) */
66
65
  export function readAndDeleteCmd(): { action: CmdAction } | null {
67
- const claimed = CMD_FILE + ".claimed";
66
+ const claimed = CMD_FILE() + ".claimed";
68
67
  try {
69
- renameSync(CMD_FILE, claimed); // atomic claim — second caller gets ENOENT
68
+ renameSync(CMD_FILE(), claimed); // atomic claim — second caller gets ENOENT
70
69
  const cmd = JSON.parse(readFileSync(claimed, "utf-8"));
71
70
  unlinkSync(claimed);
72
71
  return cmd;
@@ -78,31 +77,31 @@ export function readAndDeleteCmd(): { action: CmdAction } | null {
78
77
  }
79
78
 
80
79
  export function writeCmd(action: CmdAction) {
81
- writeFileSync(CMD_FILE, JSON.stringify({ action }));
80
+ writeFileSync(CMD_FILE(), JSON.stringify({ action }));
82
81
  }
83
82
 
84
83
  // ─── Lockfile ──────────────────────────────────────────────────────────
85
84
  export function acquireLock(): boolean {
86
85
  try {
87
86
  // Try exclusive create — fails if file already exists (atomic)
88
- const fd = openSync(LOCK_FILE, "wx");
87
+ const fd = openSync(LOCK_FILE(), "wx");
89
88
  writeFileSync(fd, String(process.pid));
90
89
  closeSync(fd);
91
90
  return true;
92
91
  } catch {
93
92
  // File exists — check if holding process is alive
94
93
  try {
95
- const pid = parseInt(readFileSync(LOCK_FILE, "utf-8").trim(), 10);
94
+ const pid = parseInt(readFileSync(LOCK_FILE(), "utf-8").trim(), 10);
96
95
  if (!isNaN(pid)) {
97
96
  try { process.kill(pid, 0); return false; } catch {} // stale lock
98
97
  }
99
98
  // Stale lock — overwrite
100
- writeFileSync(LOCK_FILE, String(process.pid));
99
+ writeFileSync(LOCK_FILE(), String(process.pid));
101
100
  return true;
102
101
  } catch { return false; }
103
102
  }
104
103
  }
105
104
 
106
105
  export function releaseLock() {
107
- try { unlinkSync(LOCK_FILE); } catch {}
106
+ try { unlinkSync(LOCK_FILE()); } catch {}
108
107
  }
@@ -4,13 +4,11 @@
4
4
  */
5
5
  import { appendFileSync } from "node:fs";
6
6
  import { resolve } from "node:path";
7
- import { homedir } from "node:os";
8
-
9
- const LOG_FILE = resolve(process.env.PPM_HOME || resolve(homedir(), ".ppm"), "ppm.log");
7
+ import { getPpmDir } from "./ppm-dir.ts";
10
8
 
11
9
  function log(level: string, msg: string) {
12
10
  const ts = new Date().toISOString();
13
- try { appendFileSync(LOG_FILE, `[${ts}] [${level}] [stopped-page] ${msg}\n`); } catch {}
11
+ try { appendFileSync(resolve(getPpmDir(), "ppm.log"), `[${ts}] [${level}] [stopped-page] ${msg}\n`); } catch {}
14
12
  }
15
13
 
16
14
  const STOPPED_HTML = `<!DOCTYPE html>
@@ -6,11 +6,11 @@
6
6
  */
7
7
  import type { Subprocess } from "bun";
8
8
  import { resolve } from "node:path";
9
- import { homedir } from "node:os";
10
9
  import {
11
10
  readFileSync, writeFileSync, existsSync, mkdirSync, openSync, appendFileSync,
12
11
  unlinkSync,
13
12
  } from "node:fs";
13
+ import { getPpmDir } from "./ppm-dir.ts";
14
14
  import { isCompiledBinary } from "./autostart-generator.ts";
15
15
  import {
16
16
  type SupervisorState,
@@ -34,9 +34,8 @@ const UPGRADE_CHECK_INTERVAL_MS = 900_000; // 15min
34
34
  const UPGRADE_SKIP_INITIAL_MS = 300_000; // 5min delay before first check
35
35
  const SELF_REPLACE_TIMEOUT_MS = 30_000; // 30s to wait for new supervisor
36
36
 
37
- const PPM_DIR = resolve(process.env.PPM_HOME || resolve(homedir(), ".ppm"));
38
- const LOG_FILE = resolve(PPM_DIR, "ppm.log");
39
- const RESTARTING_FLAG = resolve(PPM_DIR, ".restarting");
37
+ const logFile = () => resolve(getPpmDir(), "ppm.log");
38
+ const restartingFlag = () => resolve(getPpmDir(), ".restarting");
40
39
 
41
40
  // ─── State ─────────────────────────────────────────────────────────────
42
41
  let serverChild: Subprocess | null = null;
@@ -75,7 +74,7 @@ let originalArgv: string[] = [];
75
74
  function log(level: string, msg: string) {
76
75
  const ts = new Date().toISOString();
77
76
  const line = `[${ts}] [${level}] [supervisor] ${msg}\n`;
78
- try { appendFileSync(LOG_FILE, line); } catch {}
77
+ try { appendFileSync(logFile(), line); } catch {}
79
78
  if (level === "ERROR" || level === "FATAL") {
80
79
  process.stderr.write(line);
81
80
  }
@@ -103,7 +102,7 @@ export async function spawnServer(
103
102
 
104
103
  const childPid = serverChild.pid;
105
104
  updateStatus({ pid: childPid });
106
- writeFileSync(PID_FILE, String(process.pid)); // supervisor PID for stop
105
+ writeFileSync(PID_FILE(), String(process.pid)); // supervisor PID for stop
107
106
  log("INFO", `Server started (PID: ${childPid})`);
108
107
 
109
108
  const exitCode = await serverChild.exited;
@@ -413,7 +412,7 @@ async function selfReplace(): Promise<{ success: boolean; error?: string }> {
413
412
  updateStatus({ state: "upgrading" });
414
413
 
415
414
  // Set restarting flag so server child's stopTunnel() skips killing the tunnel
416
- try { writeFileSync(RESTARTING_FLAG, ""); } catch {}
415
+ try { writeFileSync(restartingFlag(), ""); } catch {}
417
416
 
418
417
  // Clear probe timer FIRST to prevent race between flush check and queued callback
419
418
  if (tunnelProbeTimer) { clearInterval(tunnelProbeTimer); tunnelProbeTimer = null; }
@@ -439,7 +438,7 @@ async function selfReplace(): Promise<{ success: boolean; error?: string }> {
439
438
 
440
439
  // Spawn new supervisor using saved argv
441
440
  const cmd = originalArgv.slice();
442
- const logFd = openSync(LOG_FILE, "a");
441
+ const logFd = openSync(logFile(), "a");
443
442
  const child = Bun.spawn({
444
443
  cmd,
445
444
  stdio: ["ignore", logFd, logFd],
@@ -452,7 +451,7 @@ async function selfReplace(): Promise<{ success: boolean; error?: string }> {
452
451
  while (Date.now() - start < SELF_REPLACE_TIMEOUT_MS) {
453
452
  await Bun.sleep(1000);
454
453
  try {
455
- const data = JSON.parse(readFileSync(STATUS_FILE, "utf-8"));
454
+ const data = JSON.parse(readFileSync(STATUS_FILE(), "utf-8"));
456
455
  if (data.supervisorPid && data.supervisorPid !== currentSupervisorPid) {
457
456
  log("INFO", `New supervisor detected (PID: ${data.supervisorPid}), old exiting`);
458
457
  // Children already killed, just clear remaining timers and exit
@@ -467,7 +466,7 @@ async function selfReplace(): Promise<{ success: boolean; error?: string }> {
467
466
  // Timeout — new supervisor didn't start, restore old supervisor
468
467
  log("ERROR", "Self-replace timeout: new supervisor did not start");
469
468
  try { child.kill(); } catch {}
470
- try { unlinkSync(RESTARTING_FLAG); } catch {}
469
+ try { unlinkSync(restartingFlag()); } catch {}
471
470
  shuttingDown = false;
472
471
  notifyStateChange("upgrading", "running", "upgrade_failed");
473
472
  setState("running");
@@ -475,7 +474,7 @@ async function selfReplace(): Promise<{ success: boolean; error?: string }> {
475
474
  return { success: false, error: "New supervisor failed to start within 30s" };
476
475
  } catch (e) {
477
476
  log("ERROR", `Self-replace error: ${e}`);
478
- try { unlinkSync(RESTARTING_FLAG); } catch {}
477
+ try { unlinkSync(restartingFlag()); } catch {}
479
478
  shuttingDown = false;
480
479
  notifyStateChange("upgrading", "running", "upgrade_failed");
481
480
  setState("running");
@@ -741,15 +740,16 @@ export async function runSupervisor(opts: {
741
740
  profile?: string;
742
741
  share: boolean;
743
742
  }) {
744
- if (!existsSync(PPM_DIR)) mkdirSync(PPM_DIR, { recursive: true });
743
+ const ppmDir = getPpmDir();
744
+ if (!existsSync(ppmDir)) mkdirSync(ppmDir, { recursive: true });
745
745
 
746
746
  // Clean up restarting flag from previous upgrade/restart
747
- try { unlinkSync(RESTARTING_FLAG); } catch {}
747
+ try { unlinkSync(restartingFlag()); } catch {}
748
748
 
749
749
  // Save original argv for self-replace
750
750
  originalArgv = [...process.argv];
751
751
 
752
- const logFd = openSync(LOG_FILE, "a");
752
+ const logFd = openSync(logFile(), "a");
753
753
  log("INFO", `Supervisor started (PID: ${process.pid}, port: ${opts.port}, share: ${opts.share})`);
754
754
 
755
755
  // Global exception handlers — supervisor must never crash
@@ -761,7 +761,7 @@ export async function runSupervisor(opts: {
761
761
  });
762
762
 
763
763
  // Write supervisor PID + clear stale availableVersion from previous run
764
- writeFileSync(PID_FILE, String(process.pid));
764
+ writeFileSync(PID_FILE(), String(process.pid));
765
765
  updateStatus({
766
766
  supervisorPid: process.pid, port: opts.port, host: opts.host, availableVersion: null,
767
767
  state: "running", pausedAt: null, pauseReason: null, lastCrashError: null,
@@ -1,12 +1,11 @@
1
1
  import type { Subprocess } from "bun";
2
2
  import { resolve } from "node:path";
3
- import { homedir } from "node:os";
4
3
  import { existsSync, unlinkSync, readFileSync, writeFileSync, renameSync } from "node:fs";
5
4
  import { ensureCloudflared } from "./cloudflared.service.ts";
5
+ import { getPpmDir } from "./ppm-dir.ts";
6
6
 
7
7
  const TUNNEL_URL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
8
8
  const decoder = new TextDecoder();
9
- const RESTARTING_FLAG = resolve(homedir(), ".ppm", ".restarting");
10
9
 
11
10
  /** Extract tunnel URL from cloudflared stderr output */
12
11
  export function extractTunnelUrl(text: string): string | null {
@@ -98,7 +97,7 @@ class TunnelService {
98
97
  this.cleanupHandler = null;
99
98
  }
100
99
  // If server is restarting, keep tunnel alive
101
- if (existsSync(RESTARTING_FLAG)) {
100
+ if (existsSync(resolve(getPpmDir(), ".restarting"))) {
102
101
  this.childProcess = null;
103
102
  this.externalPid = null;
104
103
  this.url = null;
@@ -123,11 +122,29 @@ class TunnelService {
123
122
  this.stopCloudSync();
124
123
  }
125
124
 
126
- /** Get current tunnel URL (null if not running) */
125
+ /** Get current tunnel URL (null if not running).
126
+ * Falls back to status.json if the supervisor set the URL after this process started. */
127
127
  getTunnelUrl(): string | null {
128
+ if (this.url) return this.url;
129
+ // Lazy sync: supervisor may have written shareUrl after server startup
130
+ this.syncFromStatusFile();
128
131
  return this.url;
129
132
  }
130
133
 
134
+ /** Re-read tunnel state from status.json (supervisor may have updated it after server started) */
135
+ private syncFromStatusFile(): void {
136
+ if (this.url) return; // already have a URL
137
+ try {
138
+ const statusFile = resolve(getPpmDir(), "status.json");
139
+ const status = JSON.parse(readFileSync(statusFile, "utf-8"));
140
+ if (status.shareUrl) {
141
+ this.url = status.shareUrl;
142
+ this.supervisorManaged = true;
143
+ if (status.tunnelPid) this.externalPid = status.tunnelPid;
144
+ }
145
+ } catch {}
146
+ }
147
+
131
148
  /** Get cloudflared PID (child process or external) */
132
149
  getTunnelPid(): number | null {
133
150
  return this.childProcess?.pid ?? this.externalPid;
@@ -148,7 +165,7 @@ class TunnelService {
148
165
 
149
166
  /** Persist shareUrl + tunnelPid to status.json (atomic write to avoid cross-process races) */
150
167
  private persistToStatusFile(): void {
151
- const statusFile = resolve(homedir(), ".ppm", "status.json");
168
+ const statusFile = resolve(getPpmDir(), "status.json");
152
169
  if (!existsSync(statusFile)) return;
153
170
  try {
154
171
  const data = JSON.parse(readFileSync(statusFile, "utf-8"));
@@ -3,14 +3,13 @@
3
3
  * detects install method, runs install command.
4
4
  */
5
5
  import { resolve } from "node:path";
6
- import { homedir } from "node:os";
7
6
  import { readFileSync } from "node:fs";
8
7
  import { VERSION } from "../version.ts";
9
8
  import { isCompiledBinary } from "./autostart-generator.ts";
9
+ import { getPpmDir } from "./ppm-dir.ts";
10
10
 
11
11
  const NPM_REGISTRY_URL = "https://registry.npmjs.org/@hienlh/ppm/latest";
12
12
  const FETCH_TIMEOUT_MS = 10_000;
13
- const STATUS_FILE = resolve(process.env.PPM_HOME || resolve(homedir(), ".ppm"), "status.json");
14
13
 
15
14
  export type InstallMethod = "bun" | "npm" | "binary";
16
15
 
@@ -103,7 +102,7 @@ export async function applyUpgrade(): Promise<{
103
102
  /** Send SIGUSR1 to supervisor to trigger self-replace after upgrade */
104
103
  export function signalSupervisorUpgrade(): { sent: boolean; error?: string } {
105
104
  try {
106
- const data = JSON.parse(readFileSync(STATUS_FILE, "utf-8"));
105
+ const data = JSON.parse(readFileSync(resolve(getPpmDir(), "status.json"), "utf-8"));
107
106
  const pid = data.supervisorPid;
108
107
  if (!pid) return { sent: false, error: "No supervisor PID" };
109
108
  process.kill(pid, 0); // check alive
package/src/types/chat.ts CHANGED
@@ -116,7 +116,7 @@ export type ChatEvent =
116
116
  | { type: "tool_result"; output: string; isError?: boolean; toolUseId?: string; parentToolUseId?: string }
117
117
  | { type: "approval_request"; requestId: string; tool: string; input: unknown }
118
118
  | { type: "error"; message: string }
119
- | { type: "done"; sessionId: string; resultSubtype?: ResultSubtype; numTurns?: number; contextWindowPct?: number }
119
+ | { type: "done"; sessionId: string; resultSubtype?: ResultSubtype; numTurns?: number; contextWindowPct?: number; lastMessageUuid?: string }
120
120
  | { type: "account_info"; accountId: string; accountLabel: string }
121
121
  | { type: "account_retry"; reason: string; accountId?: string; accountLabel?: string }
122
122
  | { type: "status_update"; phase: "routing" | "refreshing" | "switching"; message: string; accountLabel?: string }
@@ -139,4 +139,6 @@ export interface ChatMessage {
139
139
  /** Account used to generate this assistant message */
140
140
  accountId?: string;
141
141
  accountLabel?: string;
142
+ /** SDK message UUID — used for fork/rewind (maps to JSONL message IDs) */
143
+ sdkUuid?: string;
142
144
  }
@@ -24,7 +24,6 @@ interface TeamActivityState {
24
24
  interface ChatHistoryBarProps {
25
25
  projectName: string;
26
26
  usageInfo: UsageInfo;
27
- compactStatus?: "compacting" | null;
28
27
  usageLoading?: boolean;
29
28
  refreshUsage?: () => void;
30
29
  lastFetchedAt?: string | null;
@@ -95,7 +94,7 @@ function DebugCopyButton({ sessionId, projectName }: { sessionId: string; projec
95
94
  }
96
95
 
97
96
  export function ChatHistoryBar({
98
- projectName, usageInfo, compactStatus, usageLoading, refreshUsage, lastFetchedAt,
97
+ projectName, usageInfo, usageLoading, refreshUsage, lastFetchedAt,
99
98
  sessionId, providerId, onSelectSession, onBugReport, isConnected, onReconnect,
100
99
  teamActivity, teamMessages, onTeamOpen,
101
100
  }: ChatHistoryBarProps) {
@@ -290,20 +289,8 @@ export function ChatHistoryBar({
290
289
  <span>5h:{fiveHourPct != null ? `${fiveHourPct}%` : "--%"}</span>
291
290
  <span className="text-text-subtle">·</span>
292
291
  <span>Wk:{sevenDayPct != null ? `${sevenDayPct}%` : "--%"}</span>
293
- {compactStatus === "compacting" && (
294
- <>
295
- <span className="text-text-subtle">·</span>
296
- <span className="text-blue-400 animate-pulse">compacting...</span>
297
- </>
298
- )}
299
292
  </button>
300
- ) : (
301
- compactStatus === "compacting" ? (
302
- <span className="text-[11px] px-1.5 py-0.5 text-blue-400 animate-pulse">
303
- compacting...
304
- </span>
305
- ) : null
306
- )}
293
+ ) : null}
307
294
 
308
295
  {/* Team activity */}
309
296
  {teamActivity?.hasTeams && (
@@ -36,6 +36,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
36
36
  const [slashItems, setSlashItems] = useState<SlashItem[]>([]);
37
37
  const [showSlashPicker, setShowSlashPicker] = useState(false);
38
38
  const [slashFilter, setSlashFilter] = useState("");
39
+ const [slashRanked, setSlashRanked] = useState(false);
39
40
  const [slashSelected, setSlashSelected] = useState<SlashItem | null>(null);
40
41
 
41
42
  // File picker state
@@ -243,6 +244,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
243
244
  const handleSlashStateChange = useCallback((visible: boolean, filter: string) => {
244
245
  setShowSlashPicker(visible);
245
246
  setSlashFilter(filter);
247
+ if (!visible || !filter) setSlashRanked(false);
246
248
  }, []);
247
249
 
248
250
  const handleSlashSelect = useCallback((item: SlashItem) => {
@@ -347,6 +349,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
347
349
  phase={phase}
348
350
  connectingElapsed={connectingElapsed}
349
351
  statusMessage={statusMessage}
352
+ compactStatus={compactStatus}
350
353
  projectName={projectName}
351
354
  onFork={!isStreaming ? handleFork : undefined}
352
355
  onSelectSession={handleSelectSession}
@@ -358,7 +361,6 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
358
361
  <ChatHistoryBar
359
362
  projectName={projectName}
360
363
  usageInfo={usageInfo}
361
- compactStatus={compactStatus}
362
364
  usageLoading={usageLoading}
363
365
  refreshUsage={refreshUsage}
364
366
  lastFetchedAt={lastFetchedAt}
@@ -383,6 +385,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
383
385
  onSelect={handleSlashSelect}
384
386
  onClose={handleSlashClose}
385
387
  visible={showSlashPicker}
388
+ ranked={slashRanked}
386
389
  />
387
390
  <FilePicker
388
391
  items={fileItems}
@@ -404,7 +407,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
404
407
  initialValue={forkDraft}
405
408
  projectName={projectName}
406
409
  onSlashStateChange={handleSlashStateChange}
407
- onSlashItemsLoaded={setSlashItems}
410
+ onSlashItemsLoaded={(items, ranked) => { setSlashItems(items); if (ranked !== undefined) setSlashRanked(ranked); }}
408
411
  slashSelected={slashSelected}
409
412
  onFileStateChange={handleFileStateChange}
410
413
  onFileItemsLoaded={setFileItems}