@hienlh/ppm 0.12.11 → 0.13.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 (64) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +11 -0
  3. package/assets/skills/ppm/SKILL.md +74 -0
  4. package/assets/skills/ppm/references/cli-reference.md +728 -0
  5. package/assets/skills/ppm/references/common-tasks.md +139 -0
  6. package/assets/skills/ppm/references/http-api.md +204 -0
  7. package/bun.lock +2062 -0
  8. package/bunfig.toml +2 -0
  9. package/dist/web/assets/{audio-preview-DnQmf9fu.js → audio-preview-J5neETTY.js} +1 -1
  10. package/dist/web/assets/chat-tab-sVHRa1Fz.js +12 -0
  11. package/dist/web/assets/{code-editor-B-lU1fz3.js → code-editor-tMfcFaQ5.js} +2 -2
  12. package/dist/web/assets/{conflict-editor-BYzf3LuW.js → conflict-editor-FydCxWTC.js} +1 -1
  13. package/dist/web/assets/{database-viewer-DjvnIn8p.js → database-viewer-Celi1puH.js} +1 -1
  14. package/dist/web/assets/diff-viewer-NgDJLTk9.js +4 -0
  15. package/dist/web/assets/{extension-webview-4xMREn_x.js → extension-webview-xWAdCj3q.js} +1 -1
  16. package/dist/web/assets/{image-preview-CkS2PVdQ.js → image-preview-C6bFkdZD.js} +1 -1
  17. package/dist/web/assets/index-BMhiElt6.css +2 -0
  18. package/dist/web/assets/{index-FGlF8IWZ.js → index-DtbAoxyy.js} +2 -2
  19. package/dist/web/assets/{markdown-renderer-Bj2B05Km.js → markdown-renderer-BAnnk1pI.js} +1 -1
  20. package/dist/web/assets/{pdf-preview-CCyw5cuH.js → pdf-preview-BNuFTSOL.js} +1 -1
  21. package/dist/web/assets/{port-forwarding-tab-Cebb5Eix.js → port-forwarding-tab-BbDlGxAs.js} +1 -1
  22. package/dist/web/assets/{postgres-viewer-BrOiliEv.js → postgres-viewer-Cman1YRO.js} +1 -1
  23. package/dist/web/assets/{settings-tab-D0XjupJm.js → settings-tab-n5X_Dbu4.js} +1 -1
  24. package/dist/web/assets/{sqlite-viewer-OEVq_-Po.js → sqlite-viewer-D6JT11uu.js} +1 -1
  25. package/dist/web/assets/{terminal-tab-MjmJaQyA.js → terminal-tab-B4kMthYo.js} +1 -1
  26. package/dist/web/assets/{video-preview-B819qvlp.js → video-preview-BftQOOzF.js} +1 -1
  27. package/dist/web/index.html +2 -2
  28. package/dist/web/sw.js +1 -1
  29. package/docs/project-changelog.md +15 -1
  30. package/package.json +3 -3
  31. package/scripts/generate-ppm-skill.ts +23 -0
  32. package/scripts/lib/generate-cli-reference.ts +81 -0
  33. package/scripts/lib/generate-common-tasks.ts +14 -0
  34. package/scripts/lib/generate-http-api.ts +145 -0
  35. package/scripts/lib/generate-skill-md.ts +28 -0
  36. package/scripts/lib/write-output.ts +17 -0
  37. package/src/cli/commands/export-cmd.ts +85 -0
  38. package/src/index.ts +167 -153
  39. package/src/providers/claude-agent-sdk.ts +1 -135
  40. package/src/server/index.ts +2 -1
  41. package/src/server/routes/chat.ts +18 -0
  42. package/src/server/routes/git.ts +16 -0
  43. package/src/services/git.service.ts +34 -0
  44. package/src/services/jsonl-transcript-parser.ts +216 -0
  45. package/src/services/skill-export/backup-existing.ts +33 -0
  46. package/src/services/skill-export/copy-bundled-skill.ts +36 -0
  47. package/src/services/skill-export/generate-db-schema.ts +66 -0
  48. package/src/services/skill-export/index.ts +6 -0
  49. package/src/services/skill-export/resolve-assets-dir.ts +31 -0
  50. package/src/services/skill-export/resolve-target-dir.ts +17 -0
  51. package/src/services/supervisor.ts +2 -1
  52. package/src/web/components/chat/chat-tab.tsx +6 -1
  53. package/src/web/components/chat/message-list.tsx +101 -9
  54. package/src/web/components/chat/pre-compact-button.tsx +50 -0
  55. package/src/web/components/editor/diff-viewer.tsx +21 -5
  56. package/src/web/hooks/use-chat.ts +37 -1
  57. package/src/web/lib/flatten-expansions.ts +36 -0
  58. package/templates/skill/SKILL.md.tmpl +74 -0
  59. package/templates/skill/common-tasks.md +139 -0
  60. package/assets/skills/ppm-guide/SKILL.md +0 -61
  61. package/dist/web/assets/chat-tab-Cf6T3mGO.js +0 -12
  62. package/dist/web/assets/diff-viewer-CP2jcR5J.js +0 -4
  63. package/dist/web/assets/index-BTjuH4fn.css +0 -2
  64. package/scripts/generate-ppm-guide.ts +0 -92
package/src/index.ts CHANGED
@@ -3,156 +3,170 @@
3
3
  import { Command } from "commander";
4
4
  import { VERSION } from "./version.ts";
5
5
 
6
- const program = new Command();
7
-
8
- program
9
- .name("ppm")
10
- .description("Personal Project Manager mobile-first web IDE")
11
- .version(VERSION)
12
- .hook("preAction", () => {
13
- console.log(` PPM v${VERSION}\n`);
14
- });
15
-
16
- program
17
- .command("start")
18
- .description("Start the PPM server (background by default)")
19
- .option("-p, --port <port>", "Port to listen on")
20
- .option("-s, --share", "(deprecated) Tunnel is now always enabled")
21
- .option("--profile <name>", "DB profile name (e.g. 'dev' → ppm.dev.db)")
22
- .action(async (options) => {
23
- // Set DB profile before any DB access
24
- const { setDbProfile } = await import("./services/db.service.ts");
25
- if (options.profile) {
26
- setDbProfile(options.profile);
27
- }
28
- // Auto-init on first run
29
- const { hasConfig, initProject } = await import("./cli/commands/init.ts");
30
- if (!hasConfig()) {
31
- await initProject();
32
- }
33
- const { startServer } = await import("./server/index.ts");
34
- await startServer(options);
35
- });
36
-
37
- program
38
- .command("stop")
39
- .description("Stop the PPM server (supervisor stays alive)")
40
- .option("-a, --all", "Kill all PPM and cloudflared processes (including untracked)")
41
- .option("--kill", "Full shutdown (kills supervisor too)")
42
- .action(async (options) => {
43
- const { stopServer } = await import("./cli/commands/stop.ts");
44
- await stopServer(options);
45
- });
46
-
47
- program
48
- .command("down")
49
- .description("Fully shut down PPM (supervisor + server + tunnel)")
50
- .action(async () => {
51
- const { stopServer } = await import("./cli/commands/stop.ts");
52
- await stopServer({ kill: true });
53
- });
54
-
55
- program
56
- .command("restart")
57
- .description("Restart the server (keeps tunnel alive)")
58
- .option("--force", "Force resume from paused state")
59
- .action(async (options) => {
60
- const { restartServer } = await import("./cli/commands/restart.ts");
61
- await restartServer(options);
62
- });
63
-
64
- program
65
- .command("status")
66
- .description("Show PPM daemon status")
67
- .option("-a, --all", "Show all PPM and cloudflared processes (including untracked)")
68
- .option("--json", "Output as JSON")
69
- .action(async (options) => {
70
- const { showStatus } = await import("./cli/commands/status.ts");
71
- await showStatus(options);
72
- });
73
-
74
- program
75
- .command("open")
76
- .description("Open PPM in browser")
77
- .action(async () => {
78
- const { openBrowser } = await import("./cli/commands/open.ts");
79
- await openBrowser();
80
- });
81
-
82
- program
83
- .command("logs")
84
- .description("View PPM daemon logs")
85
- .option("-n, --tail <lines>", "Number of lines to show", "50")
86
- .option("-f, --follow", "Follow log output")
87
- .option("--clear", "Clear log file")
88
- .action(async (options) => {
89
- const { showLogs } = await import("./cli/commands/logs.ts");
90
- await showLogs(options);
91
- });
92
-
93
- program
94
- .command("report")
95
- .description("Report a bug on GitHub (pre-fills env info + logs)")
96
- .action(async () => {
97
- const { reportBug } = await import("./cli/commands/report.ts");
98
- await reportBug();
99
- });
100
-
101
- program
102
- .command("init")
103
- .description("Initialize PPM configuration (interactive or via flags)")
104
- .option("-p, --port <port>", "Port to listen on")
105
- .option("--scan <path>", "Directory to scan for git repos")
106
- .option("--auth", "Enable authentication")
107
- .option("--no-auth", "Disable authentication")
108
- .option("--password <pw>", "Set access password")
109
- .option("--share", "Pre-install cloudflared for sharing")
110
- .option("-y, --yes", "Non-interactive mode (use defaults + flags)")
111
- .action(async (options) => {
112
- const { initProject } = await import("./cli/commands/init.ts");
113
- await initProject(options);
114
- });
115
-
116
- const { registerProjectsCommands } = await import("./cli/commands/projects.ts");
117
- registerProjectsCommands(program);
118
-
119
- const { registerConfigCommands } = await import("./cli/commands/config-cmd.ts");
120
- registerConfigCommands(program);
121
-
122
- const { registerGitCommands } = await import("./cli/commands/git-cmd.ts");
123
- registerGitCommands(program);
124
-
125
- const { registerChatCommands } = await import("./cli/commands/chat-cmd.ts");
126
- registerChatCommands(program);
127
-
128
- program
129
- .command("upgrade")
130
- .description("Check for and install PPM updates")
131
- .option("--check", "Only check for updates, don't install")
132
- .action(async (options) => {
133
- const { upgradeCmd } = await import("./cli/commands/upgrade.ts");
134
- await upgradeCmd(options);
135
- });
136
-
137
- const { registerAutoStartCommands } = await import("./cli/commands/autostart.ts");
138
- registerAutoStartCommands(program);
139
-
140
- const { registerCloudCommands } = await import("./cli/commands/cloud.ts");
141
- registerCloudCommands(program);
142
-
143
- const { registerSkillsCommands } = await import("./cli/commands/skills-cmd.ts");
144
- registerSkillsCommands(program);
145
-
146
- const { registerExtCommands } = await import("./cli/commands/ext-cmd.ts");
147
- registerExtCommands(program);
148
-
149
- const { registerDbCommands } = await import("./cli/commands/db-cmd.ts");
150
- registerDbCommands(program);
151
-
152
- const { registerBotCommands } = await import("./cli/commands/bot-cmd.ts");
153
- registerBotCommands(program);
154
-
155
- const { registerJiraCommands } = await import("./cli/commands/jira-cmd.ts");
156
- await registerJiraCommands(program);
157
-
158
- program.parse();
6
+ /**
7
+ * Assemble the CLI program without parsing argv. Exported so build-time tools
8
+ * (e.g. scripts/generate-ppm-skill.ts) can introspect the Commander tree for
9
+ * auto-generated documentation. `preAction` hooks and action callbacks are
10
+ * registered but not invoked until `.parseAsync()` runs.
11
+ */
12
+ export async function buildProgram(): Promise<Command> {
13
+ const program = new Command();
14
+
15
+ program
16
+ .name("ppm")
17
+ .description("Personal Project Manager — mobile-first web IDE")
18
+ .version(VERSION)
19
+ .hook("preAction", () => {
20
+ console.log(` PPM v${VERSION}\n`);
21
+ });
22
+
23
+ program
24
+ .command("start")
25
+ .description("Start the PPM server (background by default)")
26
+ .option("-p, --port <port>", "Port to listen on")
27
+ .option("-s, --share", "(deprecated) Tunnel is now always enabled")
28
+ .option("--profile <name>", "DB profile name (e.g. 'dev' → ppm.dev.db)")
29
+ .action(async (options) => {
30
+ const { setDbProfile } = await import("./services/db.service.ts");
31
+ if (options.profile) {
32
+ setDbProfile(options.profile);
33
+ }
34
+ const { hasConfig, initProject } = await import("./cli/commands/init.ts");
35
+ if (!hasConfig()) {
36
+ await initProject();
37
+ }
38
+ const { startServer } = await import("./server/index.ts");
39
+ await startServer(options);
40
+ });
41
+
42
+ program
43
+ .command("stop")
44
+ .description("Stop the PPM server (supervisor stays alive)")
45
+ .option("-a, --all", "Kill all PPM and cloudflared processes (including untracked)")
46
+ .option("--kill", "Full shutdown (kills supervisor too)")
47
+ .action(async (options) => {
48
+ const { stopServer } = await import("./cli/commands/stop.ts");
49
+ await stopServer(options);
50
+ });
51
+
52
+ program
53
+ .command("down")
54
+ .description("Fully shut down PPM (supervisor + server + tunnel)")
55
+ .action(async () => {
56
+ const { stopServer } = await import("./cli/commands/stop.ts");
57
+ await stopServer({ kill: true });
58
+ });
59
+
60
+ program
61
+ .command("restart")
62
+ .description("Restart the server (keeps tunnel alive)")
63
+ .option("--force", "Force resume from paused state")
64
+ .action(async (options) => {
65
+ const { restartServer } = await import("./cli/commands/restart.ts");
66
+ await restartServer(options);
67
+ });
68
+
69
+ program
70
+ .command("status")
71
+ .description("Show PPM daemon status")
72
+ .option("-a, --all", "Show all PPM and cloudflared processes (including untracked)")
73
+ .option("--json", "Output as JSON")
74
+ .action(async (options) => {
75
+ const { showStatus } = await import("./cli/commands/status.ts");
76
+ await showStatus(options);
77
+ });
78
+
79
+ program
80
+ .command("open")
81
+ .description("Open PPM in browser")
82
+ .action(async () => {
83
+ const { openBrowser } = await import("./cli/commands/open.ts");
84
+ await openBrowser();
85
+ });
86
+
87
+ program
88
+ .command("logs")
89
+ .description("View PPM daemon logs")
90
+ .option("-n, --tail <lines>", "Number of lines to show", "50")
91
+ .option("-f, --follow", "Follow log output")
92
+ .option("--clear", "Clear log file")
93
+ .action(async (options) => {
94
+ const { showLogs } = await import("./cli/commands/logs.ts");
95
+ await showLogs(options);
96
+ });
97
+
98
+ program
99
+ .command("report")
100
+ .description("Report a bug on GitHub (pre-fills env info + logs)")
101
+ .action(async () => {
102
+ const { reportBug } = await import("./cli/commands/report.ts");
103
+ await reportBug();
104
+ });
105
+
106
+ program
107
+ .command("init")
108
+ .description("Initialize PPM configuration (interactive or via flags)")
109
+ .option("-p, --port <port>", "Port to listen on")
110
+ .option("--scan <path>", "Directory to scan for git repos")
111
+ .option("--auth", "Enable authentication")
112
+ .option("--no-auth", "Disable authentication")
113
+ .option("--password <pw>", "Set access password")
114
+ .option("--share", "Pre-install cloudflared for sharing")
115
+ .option("-y, --yes", "Non-interactive mode (use defaults + flags)")
116
+ .action(async (options) => {
117
+ const { initProject } = await import("./cli/commands/init.ts");
118
+ await initProject(options);
119
+ });
120
+
121
+ const { registerProjectsCommands } = await import("./cli/commands/projects.ts");
122
+ registerProjectsCommands(program);
123
+
124
+ const { registerConfigCommands } = await import("./cli/commands/config-cmd.ts");
125
+ registerConfigCommands(program);
126
+
127
+ const { registerGitCommands } = await import("./cli/commands/git-cmd.ts");
128
+ registerGitCommands(program);
129
+
130
+ const { registerChatCommands } = await import("./cli/commands/chat-cmd.ts");
131
+ registerChatCommands(program);
132
+
133
+ program
134
+ .command("upgrade")
135
+ .description("Check for and install PPM updates")
136
+ .option("--check", "Only check for updates, don't install")
137
+ .action(async (options) => {
138
+ const { upgradeCmd } = await import("./cli/commands/upgrade.ts");
139
+ await upgradeCmd(options);
140
+ });
141
+
142
+ const { registerAutoStartCommands } = await import("./cli/commands/autostart.ts");
143
+ registerAutoStartCommands(program);
144
+
145
+ const { registerCloudCommands } = await import("./cli/commands/cloud.ts");
146
+ registerCloudCommands(program);
147
+
148
+ const { registerSkillsCommands } = await import("./cli/commands/skills-cmd.ts");
149
+ registerSkillsCommands(program);
150
+
151
+ const { registerExtCommands } = await import("./cli/commands/ext-cmd.ts");
152
+ registerExtCommands(program);
153
+
154
+ const { registerDbCommands } = await import("./cli/commands/db-cmd.ts");
155
+ registerDbCommands(program);
156
+
157
+ const { registerBotCommands } = await import("./cli/commands/bot-cmd.ts");
158
+ registerBotCommands(program);
159
+
160
+ const { registerJiraCommands } = await import("./cli/commands/jira-cmd.ts");
161
+ await registerJiraCommands(program);
162
+
163
+ const { registerExportCommands } = await import("./cli/commands/export-cmd.ts");
164
+ registerExportCommands(program);
165
+
166
+ return program;
167
+ }
168
+
169
+ if (import.meta.main) {
170
+ const program = await buildProgram();
171
+ program.parse();
172
+ }
@@ -19,6 +19,7 @@ import { updateFromSdkEvent } from "../services/claude-usage.service.ts";
19
19
  import { getSessionProjectPath, setSessionMetadata, getSessionTitles } from "../services/db.service.ts";
20
20
  import { accountSelector } from "../services/account-selector.service.ts";
21
21
  import { accountService, type AccountWithTokens } from "../services/account.service.ts";
22
+ import { parseSessionMessage, nestChildEvents } from "../services/jsonl-transcript-parser.ts";
22
23
  import { resolve } from "node:path";
23
24
  import { existsSync, readdirSync, unlinkSync, readFileSync, statSync } from "node:fs";
24
25
  import { homedir } from "node:os";
@@ -1587,135 +1588,6 @@ export class ClaudeAgentSdkProvider implements AIProvider {
1587
1588
  }
1588
1589
  }
1589
1590
 
1590
- /** Parse SDK SessionMessage into ChatMessage with events for tool_use blocks */
1591
- function parseSessionMessage(msg: { uuid: string; type: string; message: unknown; parent_tool_use_id?: string | null }): ChatMessage {
1592
- const message = msg.message as Record<string, unknown> | undefined;
1593
- const role = msg.type as "user" | "assistant";
1594
- const parentId = (msg as any).parent_tool_use_id as string | undefined;
1595
-
1596
- // Filter synthetic SDK-generated error messages (auth failures, rate limits, etc.).
1597
- // Structure: { isApiErrorMessage: true, error: "authentication_failed"|"rate_limit"|...,
1598
- // message: { model: "<synthetic>", content: [{text: "Failed to authenticate..."}] } }
1599
- // Our retry loop handles these; the raw text must not render in chat history.
1600
- const isSdkErrorMessage =
1601
- (msg as any).isApiErrorMessage === true ||
1602
- typeof (msg as any).error === "string" ||
1603
- (message && (message as any).model === "<synthetic>" &&
1604
- Array.isArray(message.content) &&
1605
- (message.content as Array<Record<string, unknown>>).some(
1606
- (b) => b.type === "text" && typeof b.text === "string" &&
1607
- /Failed to authenticate|API Error: 40[13]|hit your limit|rate.?limit/i.test(b.text as string),
1608
- ));
1609
- if (isSdkErrorMessage) {
1610
- return {
1611
- id: msg.uuid,
1612
- role,
1613
- content: "",
1614
- timestamp: new Date().toISOString(),
1615
- sdkUuid: msg.uuid,
1616
- };
1617
- }
1618
-
1619
- // Parse content blocks for both user and assistant messages
1620
- const events: ChatEvent[] = [];
1621
- let textContent = "";
1622
-
1623
- if (message && Array.isArray(message.content)) {
1624
- for (const block of message.content as Array<Record<string, unknown>>) {
1625
- if (block.type === "text" && typeof block.text === "string") {
1626
- const cleaned = role === "assistant" ? stripTeammateXml(block.text) : block.text;
1627
- textContent += cleaned;
1628
- if (role === "assistant" && cleaned) {
1629
- events.push({ type: "text", content: cleaned, ...(parentId && { parentToolUseId: parentId }) });
1630
- }
1631
- } else if (block.type === "tool_use") {
1632
- events.push({
1633
- type: "tool_use",
1634
- tool: (block.name as string) ?? "unknown",
1635
- input: block.input ?? {},
1636
- toolUseId: block.id as string | undefined,
1637
- ...(parentId && { parentToolUseId: parentId }),
1638
- });
1639
- } else if (block.type === "tool_result") {
1640
- const output = block.content ?? block.output ?? "";
1641
- events.push({
1642
- type: "tool_result",
1643
- output: typeof output === "string" ? output : JSON.stringify(output),
1644
- isError: !!(block as Record<string, unknown>).is_error,
1645
- toolUseId: block.tool_use_id as string | undefined,
1646
- ...(parentId && { parentToolUseId: parentId }),
1647
- });
1648
- }
1649
- }
1650
- } else {
1651
- textContent = extractText(message);
1652
- }
1653
-
1654
- // SDK-generated user messages carry system text (tool_result blocks,
1655
- // <teammate-message> XML, <task-notification> XML) — not actual user input.
1656
- // Clear so they don't render as user bubbles.
1657
- if (role === "user" && (events.some((e) => e.type === "tool_result") || textContent.includes("<teammate-message"))) {
1658
- textContent = "";
1659
- }
1660
-
1661
- return {
1662
- id: msg.uuid,
1663
- role,
1664
- content: textContent,
1665
- events: events.length > 0 ? events : undefined,
1666
- timestamp: new Date().toISOString(),
1667
- sdkUuid: msg.uuid,
1668
- };
1669
- }
1670
-
1671
- /**
1672
- * Move events with parentToolUseId into their parent Agent/Task tool_use's children array.
1673
- * Mutates the array in-place: child events are removed from the top level and pushed into parent.children.
1674
- */
1675
- function nestChildEvents(events: ChatEvent[]): void {
1676
- // Build map of Agent/Task tool_use events by toolUseId
1677
- const parentMap = new Map<string, ChatEvent & { type: "tool_use" }>();
1678
- for (const ev of events) {
1679
- if (ev.type === "tool_use" && (ev.tool === "Agent" || ev.tool === "Task") && ev.toolUseId) {
1680
- parentMap.set(ev.toolUseId, ev);
1681
- }
1682
- }
1683
- if (parentMap.size === 0) return;
1684
-
1685
- // Collect indices of child events to remove
1686
- const childIndices: number[] = [];
1687
- for (let i = 0; i < events.length; i++) {
1688
- const ev = events[i]!;
1689
- const pid = (ev as any).parentToolUseId as string | undefined;
1690
- if (!pid) continue;
1691
- const parent = parentMap.get(pid);
1692
- if (parent) {
1693
- if (!parent.children) parent.children = [];
1694
- parent.children.push(ev);
1695
- childIndices.push(i);
1696
- }
1697
- }
1698
-
1699
- // Remove children from flat array (reverse order to keep indices valid)
1700
- for (let i = childIndices.length - 1; i >= 0; i--) {
1701
- events.splice(childIndices[i]!, 1);
1702
- }
1703
- }
1704
-
1705
- /** Extract plain text from message payload */
1706
- function extractText(message: unknown): string {
1707
- if (!message || typeof message !== "object") return "";
1708
- const msg = message as Record<string, unknown>;
1709
- if (typeof msg.content === "string") return msg.content;
1710
- if (Array.isArray(msg.content)) {
1711
- return (msg.content as Array<Record<string, unknown>>)
1712
- .filter((b) => b.type === "text" && typeof b.text === "string")
1713
- .map((b) => b.text as string)
1714
- .join("");
1715
- }
1716
- return "";
1717
- }
1718
-
1719
1591
  /**
1720
1592
  * Scan a JSONL project directory for sessions that the SDK's listSessions missed.
1721
1593
  * The SDK uses a 64KB head buffer; sessions with very large first messages
@@ -1791,9 +1663,3 @@ function findMissingSessions(
1791
1663
  return results;
1792
1664
  }
1793
1665
 
1794
- /** Strip SDK teammate-message XML tags from assistant text */
1795
- const TEAMMATE_MSG_RE = /<teammate-message[^>]*>[\s\S]*?<\/teammate-message>/g;
1796
- function stripTeammateXml(text: string): string {
1797
- if (!text.includes("<teammate-message")) return text;
1798
- return text.replace(TEAMMATE_MSG_RE, "").replace(/\n{3,}/g, "\n\n").trim();
1799
- }
@@ -530,7 +530,8 @@ if (process.argv.includes("__serve__")) {
530
530
  const idx = process.argv.indexOf("__serve__");
531
531
  const port = parseInt(process.argv[idx + 1] ?? "8080", 10);
532
532
  const host = process.argv[idx + 2] ?? "0.0.0.0";
533
- const profileArg = process.argv[idx + 3] && process.argv[idx + 3] !== "_" ? process.argv[idx + 3] : undefined;
533
+ const profileRaw = process.argv[idx + 3];
534
+ const profileArg = profileRaw && profileRaw !== "_" && !profileRaw.startsWith("--") ? profileRaw : undefined;
534
535
 
535
536
  // Set DB profile for daemon child
536
537
  const { setDbProfile } = await import("../services/db.service.ts");
@@ -9,6 +9,7 @@ import { listSlashItems, searchSlashItems, invalidateCache } from "../../service
9
9
  import { upsertSlashRecent, getSlashRecents } from "../../services/db.service.ts";
10
10
  import { getCachedUsage, refreshUsageNow } from "../../services/claude-usage.service.ts";
11
11
  import { getSessionLog } from "../../services/session-log.service.ts";
12
+ import { parseJsonlTranscript, validateJsonlPath } from "../../services/jsonl-transcript-parser.ts";
12
13
  import { getSessionProjectPath, setSessionMetadata, setSessionTitle, getPinnedSessionIds, pinSession, unpinSession, deleteSessionMapping, deleteSessionMetadata, deleteSessionTitle } from "../../services/db.service.ts";
13
14
  import { setSessionTag, bulkSetSessionTag, getTagById, getSessionTags, getProjectDefaultTagId } from "../../services/tag.service.ts";
14
15
  import { ok, err } from "../../types/api.ts";
@@ -363,6 +364,23 @@ chatRoutes.get("/sessions/:id/debug", (c) => {
363
364
  return c.json(ok({ sessionId, jsonlPath: jsonlExists ? jsonlPath : null, jsonlDir, projectPath }));
364
365
  });
365
366
 
367
+ /** GET /chat/pre-compact-messages — read and parse a JSONL transcript file (for expand-compact feature) */
368
+ chatRoutes.get("/pre-compact-messages", async (c) => {
369
+ try {
370
+ const jsonlPath = c.req.query("jsonlPath");
371
+ if (!jsonlPath) return c.json(err("jsonlPath query param required"), 400);
372
+ const validated = validateJsonlPath(jsonlPath);
373
+ const messages = await parseJsonlTranscript(validated);
374
+ return c.json(ok(messages));
375
+ } catch (e) {
376
+ const message = e instanceof Error ? e.message : "Unknown error";
377
+ const status = /not found/i.test(message) ? 404
378
+ : /denied|traversal|Invalid path|too large|Not a regular/i.test(message) ? 403
379
+ : 500;
380
+ return c.json(err(message), status);
381
+ }
382
+ });
383
+
366
384
  /** POST /chat/upload — upload files for chat attachments, returns server-side paths */
367
385
  chatRoutes.post("/upload", async (c) => {
368
386
  try {
@@ -57,6 +57,22 @@ gitRoutes.get("/file-diff", async (c) => {
57
57
  }
58
58
  });
59
59
 
60
+ /** GET /git/file-full-diff?file=&ref=
61
+ * Returns full file contents (VSCode-style) for both sides:
62
+ * { original: <ref version>, modified: <working tree> } */
63
+ gitRoutes.get("/file-full-diff", async (c) => {
64
+ try {
65
+ const projectPath = c.get("projectPath");
66
+ const file = c.req.query("file");
67
+ if (!file) return c.json(err("Missing query: file"), 400);
68
+ const ref = c.req.query("ref") || "HEAD";
69
+ const result = await gitService.fileFullDiff(projectPath, file, ref);
70
+ return c.json(ok(result));
71
+ } catch (e) {
72
+ return c.json(err((e as Error).message), 500);
73
+ }
74
+ });
75
+
60
76
  /** GET /git/graph?max=200&skip=0 */
61
77
  gitRoutes.get("/graph", async (c) => {
62
78
  try {
@@ -122,6 +122,40 @@ class GitService {
122
122
  return files;
123
123
  }
124
124
 
125
+ /**
126
+ * Returns full file contents for both sides of a diff (VSCode-style).
127
+ * - original: file at HEAD (empty if new/untracked/ref missing)
128
+ * - modified: working tree content (empty if deleted on disk)
129
+ * Monaco DiffEditor will compute/render the diff from these full contents.
130
+ */
131
+ async fileFullDiff(
132
+ projectPath: string,
133
+ filePath: string,
134
+ ref: string = "HEAD",
135
+ ): Promise<{ original: string; modified: string }> {
136
+ const git = this.git(projectPath);
137
+ const absPath = path.resolve(projectPath, filePath);
138
+
139
+ let original = "";
140
+ try {
141
+ original = await git.show([`${ref}:${filePath}`]);
142
+ } catch {
143
+ // File does not exist at ref (new/untracked/added) → empty original
144
+ original = "";
145
+ }
146
+
147
+ let modified = "";
148
+ try {
149
+ const f = Bun.file(absPath);
150
+ if (await f.exists()) modified = await f.text();
151
+ } catch {
152
+ // File missing on disk (deleted) → empty modified
153
+ modified = "";
154
+ }
155
+
156
+ return { original, modified };
157
+ }
158
+
125
159
  async fileDiff(
126
160
  projectPath: string,
127
161
  filePath: string,