@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.
- package/CHANGELOG.md +18 -0
- package/README.md +11 -0
- package/assets/skills/ppm/SKILL.md +74 -0
- package/assets/skills/ppm/references/cli-reference.md +728 -0
- package/assets/skills/ppm/references/common-tasks.md +139 -0
- package/assets/skills/ppm/references/http-api.md +204 -0
- package/bun.lock +2062 -0
- package/bunfig.toml +2 -0
- package/dist/web/assets/{audio-preview-DnQmf9fu.js → audio-preview-J5neETTY.js} +1 -1
- package/dist/web/assets/chat-tab-sVHRa1Fz.js +12 -0
- package/dist/web/assets/{code-editor-B-lU1fz3.js → code-editor-tMfcFaQ5.js} +2 -2
- package/dist/web/assets/{conflict-editor-BYzf3LuW.js → conflict-editor-FydCxWTC.js} +1 -1
- package/dist/web/assets/{database-viewer-DjvnIn8p.js → database-viewer-Celi1puH.js} +1 -1
- package/dist/web/assets/diff-viewer-NgDJLTk9.js +4 -0
- package/dist/web/assets/{extension-webview-4xMREn_x.js → extension-webview-xWAdCj3q.js} +1 -1
- package/dist/web/assets/{image-preview-CkS2PVdQ.js → image-preview-C6bFkdZD.js} +1 -1
- package/dist/web/assets/index-BMhiElt6.css +2 -0
- package/dist/web/assets/{index-FGlF8IWZ.js → index-DtbAoxyy.js} +2 -2
- package/dist/web/assets/{markdown-renderer-Bj2B05Km.js → markdown-renderer-BAnnk1pI.js} +1 -1
- package/dist/web/assets/{pdf-preview-CCyw5cuH.js → pdf-preview-BNuFTSOL.js} +1 -1
- package/dist/web/assets/{port-forwarding-tab-Cebb5Eix.js → port-forwarding-tab-BbDlGxAs.js} +1 -1
- package/dist/web/assets/{postgres-viewer-BrOiliEv.js → postgres-viewer-Cman1YRO.js} +1 -1
- package/dist/web/assets/{settings-tab-D0XjupJm.js → settings-tab-n5X_Dbu4.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-OEVq_-Po.js → sqlite-viewer-D6JT11uu.js} +1 -1
- package/dist/web/assets/{terminal-tab-MjmJaQyA.js → terminal-tab-B4kMthYo.js} +1 -1
- package/dist/web/assets/{video-preview-B819qvlp.js → video-preview-BftQOOzF.js} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/docs/project-changelog.md +15 -1
- package/package.json +3 -3
- package/scripts/generate-ppm-skill.ts +23 -0
- package/scripts/lib/generate-cli-reference.ts +81 -0
- package/scripts/lib/generate-common-tasks.ts +14 -0
- package/scripts/lib/generate-http-api.ts +145 -0
- package/scripts/lib/generate-skill-md.ts +28 -0
- package/scripts/lib/write-output.ts +17 -0
- package/src/cli/commands/export-cmd.ts +85 -0
- package/src/index.ts +167 -153
- package/src/providers/claude-agent-sdk.ts +1 -135
- package/src/server/index.ts +2 -1
- package/src/server/routes/chat.ts +18 -0
- package/src/server/routes/git.ts +16 -0
- package/src/services/git.service.ts +34 -0
- package/src/services/jsonl-transcript-parser.ts +216 -0
- package/src/services/skill-export/backup-existing.ts +33 -0
- package/src/services/skill-export/copy-bundled-skill.ts +36 -0
- package/src/services/skill-export/generate-db-schema.ts +66 -0
- package/src/services/skill-export/index.ts +6 -0
- package/src/services/skill-export/resolve-assets-dir.ts +31 -0
- package/src/services/skill-export/resolve-target-dir.ts +17 -0
- package/src/services/supervisor.ts +2 -1
- package/src/web/components/chat/chat-tab.tsx +6 -1
- package/src/web/components/chat/message-list.tsx +101 -9
- package/src/web/components/chat/pre-compact-button.tsx +50 -0
- package/src/web/components/editor/diff-viewer.tsx +21 -5
- package/src/web/hooks/use-chat.ts +37 -1
- package/src/web/lib/flatten-expansions.ts +36 -0
- package/templates/skill/SKILL.md.tmpl +74 -0
- package/templates/skill/common-tasks.md +139 -0
- package/assets/skills/ppm-guide/SKILL.md +0 -61
- package/dist/web/assets/chat-tab-Cf6T3mGO.js +0 -12
- package/dist/web/assets/diff-viewer-CP2jcR5J.js +0 -4
- package/dist/web/assets/index-BTjuH4fn.css +0 -2
- 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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
program
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
program
|
|
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
|
-
}
|
package/src/server/index.ts
CHANGED
|
@@ -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
|
|
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 {
|
package/src/server/routes/git.ts
CHANGED
|
@@ -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,
|