@hienlh/ppm 0.1.4 → 0.2.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/CLAUDE.md +45 -0
- package/bun.lock +55 -0
- package/dist/ppm +0 -0
- package/dist/web/assets/api-client-BgVufYKf.js +1 -0
- package/dist/web/assets/arrow-up-from-line-DjfWTP75.js +1 -0
- package/dist/web/assets/button-KIZetva8.js +41 -0
- package/dist/web/assets/chat-tab-D7dR7kbZ.js +6 -0
- package/dist/web/assets/code-editor-r8P6Gk4M.js +2 -0
- package/dist/web/assets/copy-B-kLwqzg.js +1 -0
- package/dist/web/assets/dialog-D8ulRTfX.js +5 -0
- package/dist/web/assets/diff-viewer-vSvrem_i.js +4 -0
- package/dist/web/assets/dist-C4W3AGh3.js +1 -0
- package/dist/web/assets/dist-PA84y4Ga.js +1 -0
- package/dist/web/assets/external-link-Dim3NH6h.js +1 -0
- package/dist/web/assets/git-graph-Cn-s1k0-.js +1 -0
- package/dist/web/assets/git-status-panel-QjAQzNAi.js +1 -0
- package/dist/web/assets/index-DUBI96T5.css +2 -0
- package/dist/web/assets/index-nk1dAWff.js +10 -0
- package/dist/web/assets/{jsx-runtime-BnxRlLMJ.js → jsx-runtime-BFALxl05.js} +1 -1
- package/dist/web/assets/marked.esm-Cv8mjgnt.js +59 -0
- package/dist/web/assets/project-list-DqiatpaH.js +1 -0
- package/dist/web/assets/{react-Uzd0zARU.js → react-BSLFEYu8.js} +1 -1
- package/dist/web/assets/refresh-cw-DJSjl6Ev.js +1 -0
- package/dist/web/assets/settings-tab-iCGeFFdt.js +1 -0
- package/dist/web/assets/terminal-tab-DDf6S-Tu.js +36 -0
- package/dist/web/assets/trash-2-CjahwKg8.js +1 -0
- package/dist/web/assets/x-BxhOxZ5p.js +1 -0
- package/dist/web/index.html +11 -10
- package/dist/web/sw.js +1 -1
- package/docs/claude-agent-sdk-reference.md +780 -0
- package/docs/code-standards.md +74 -0
- package/docs/codebase-summary.md +22 -20
- package/docs/deployment-guide.md +81 -11
- package/docs/lessons-learned.md +58 -0
- package/docs/project-overview-pdr.md +62 -2
- package/docs/system-architecture.md +102 -10
- package/package.json +4 -1
- package/schemas/ppm-config.schema.json +87 -0
- package/src/cli/commands/init.ts +186 -43
- package/src/cli/commands/status.ts +73 -0
- package/src/cli/commands/stop.ts +24 -10
- package/src/index.ts +28 -5
- package/src/providers/claude-agent-sdk.ts +84 -3
- package/src/providers/registry.ts +0 -2
- package/src/server/index.ts +106 -15
- package/src/server/routes/settings.ts +70 -0
- package/src/server/ws/chat.ts +8 -6
- package/src/services/cloudflared.service.ts +99 -0
- package/src/services/git.service.ts +23 -1
- package/src/services/tunnel.service.ts +100 -0
- package/src/types/chat.ts +8 -1
- package/src/types/config.ts +50 -3
- package/src/web/app.tsx +10 -2
- package/src/web/components/auth/login-screen.tsx +1 -1
- package/src/web/components/chat/message-input.tsx +1 -1
- package/src/web/components/chat/message-list.tsx +112 -251
- package/src/web/components/chat/tool-cards.tsx +411 -0
- package/src/web/components/editor/code-editor.tsx +80 -20
- package/src/web/components/editor/diff-viewer.tsx +72 -7
- package/src/web/components/git/git-graph.tsx +3 -0
- package/src/web/components/git/git-status-panel.tsx +50 -1
- package/src/web/components/layout/command-palette.tsx +215 -0
- package/src/web/components/layout/mobile-drawer.tsx +143 -42
- package/src/web/components/layout/sidebar.tsx +103 -67
- package/src/web/components/layout/tab-bar.tsx +1 -2
- package/src/web/components/settings/ai-settings-section.tsx +166 -0
- package/src/web/components/settings/settings-tab.tsx +5 -0
- package/src/web/components/terminal/terminal-tab.tsx +45 -22
- package/src/web/components/ui/input.tsx +4 -3
- package/src/web/components/ui/label.tsx +24 -0
- package/src/web/components/ui/select.tsx +188 -0
- package/src/web/hooks/use-global-keybindings.ts +56 -0
- package/src/web/hooks/use-terminal.ts +14 -1
- package/src/web/lib/api-settings.ts +24 -0
- package/src/web/stores/project-store.ts +47 -2
- package/src/web/stores/tab-store.ts +1 -1
- package/src/web/styles/globals.css +20 -6
- package/test-tool.mjs +41 -0
- package/dist/web/assets/api-client-Bnf9LAt4.js +0 -1
- package/dist/web/assets/arrow-up-from-line-BXL5dtbG.js +0 -1
- package/dist/web/assets/button-DxRZgE8F.js +0 -1
- package/dist/web/assets/chat-tab-p2mwkdec.js +0 -61
- package/dist/web/assets/code-editor-vMRyRKV3.js +0 -2
- package/dist/web/assets/createLucideIcon-Dy1wlrF7.js +0 -1
- package/dist/web/assets/dialog-Db6prp1p.js +0 -45
- package/dist/web/assets/diff-viewer-BdDje3Wr.js +0 -4
- package/dist/web/assets/external-link-WSiY-639.js +0 -1
- package/dist/web/assets/git-graph-B-qwuFoO.js +0 -1
- package/dist/web/assets/git-status-panel-NkZFb5v1.js +0 -1
- package/dist/web/assets/index-BHEFCU01.js +0 -10
- package/dist/web/assets/index-DYd_2slk.css +0 -2
- package/dist/web/assets/project-list-NkR7IHT5.js +0 -1
- package/dist/web/assets/refresh-cw-DtopuYJf.js +0 -1
- package/dist/web/assets/settings-tab-DKx0s3Q1.js +0 -1
- package/dist/web/assets/terminal-tab-DHwn2LMT.js +0 -36
- package/dist/web/assets/trash-2-CHLebaNh.js +0 -1
- package/dist/web/assets/x-BISR7bpK.js +0 -1
- package/src/providers/claude-binary-finder.ts +0 -256
- package/src/providers/claude-code-cli.ts +0 -413
- package/src/providers/claude-process-registry.ts +0 -106
- /package/dist/web/assets/{dist-CSp7ir0r.js → dist-CBiGQxfr.js} +0 -0
- /package/dist/web/assets/{utils-CiBGfeHD.js → utils-DpJF9mAi.js} +0 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "PPM Configuration",
|
|
4
|
+
"description": "Configuration file for PPM (Project & Process Manager)",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"port": {
|
|
8
|
+
"type": "integer",
|
|
9
|
+
"default": 8080,
|
|
10
|
+
"description": "Server port"
|
|
11
|
+
},
|
|
12
|
+
"host": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"default": "0.0.0.0",
|
|
15
|
+
"description": "Server bind address"
|
|
16
|
+
},
|
|
17
|
+
"auth": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"enabled": { "type": "boolean", "default": true },
|
|
21
|
+
"token": { "type": "string" }
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"projects": {
|
|
25
|
+
"type": "array",
|
|
26
|
+
"items": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"properties": {
|
|
29
|
+
"path": { "type": "string" },
|
|
30
|
+
"name": { "type": "string" }
|
|
31
|
+
},
|
|
32
|
+
"required": ["path", "name"]
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"ai": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"default_provider": { "type": "string", "default": "claude" },
|
|
39
|
+
"providers": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"additionalProperties": {
|
|
42
|
+
"$ref": "#/$defs/AIProviderConfig"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"$defs": {
|
|
49
|
+
"AIProviderConfig": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"properties": {
|
|
52
|
+
"type": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"enum": ["agent-sdk", "mock"]
|
|
55
|
+
},
|
|
56
|
+
"api_key_env": { "type": "string" },
|
|
57
|
+
"model": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"description": "Model ID (e.g. claude-sonnet-4-6, claude-opus-4-6)",
|
|
60
|
+
"examples": ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5"]
|
|
61
|
+
},
|
|
62
|
+
"effort": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"enum": ["low", "medium", "high", "max"],
|
|
65
|
+
"default": "high"
|
|
66
|
+
},
|
|
67
|
+
"max_turns": {
|
|
68
|
+
"type": "integer",
|
|
69
|
+
"minimum": 1,
|
|
70
|
+
"maximum": 500,
|
|
71
|
+
"default": 100
|
|
72
|
+
},
|
|
73
|
+
"max_budget_usd": {
|
|
74
|
+
"type": "number",
|
|
75
|
+
"minimum": 0.01,
|
|
76
|
+
"maximum": 50
|
|
77
|
+
},
|
|
78
|
+
"thinking_budget_tokens": {
|
|
79
|
+
"type": "integer",
|
|
80
|
+
"minimum": 0,
|
|
81
|
+
"description": "0 = disabled"
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"required": ["type"]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -1,57 +1,200 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
|
+
import { input, confirm, select, password } from "@inquirer/prompts";
|
|
4
5
|
import { configService } from "../../services/config.service.ts";
|
|
5
6
|
import { projectService } from "../../services/project.service.ts";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
const ppmDir = resolve(homedir(), ".ppm");
|
|
9
|
-
const globalConfig = resolve(ppmDir, "config.yaml");
|
|
8
|
+
const DEFAULT_PORT = 3210;
|
|
10
9
|
|
|
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
|
-
console.log(` [added] ${name} (${repoPath})`);
|
|
41
|
-
added++;
|
|
42
|
-
} catch (e) {
|
|
43
|
-
console.log(` [error] ${name}: ${(e as Error).message}`);
|
|
44
|
-
}
|
|
10
|
+
export interface InitOptions {
|
|
11
|
+
port?: string;
|
|
12
|
+
scan?: string;
|
|
13
|
+
auth?: boolean;
|
|
14
|
+
password?: string;
|
|
15
|
+
share?: boolean;
|
|
16
|
+
/** Skip prompts, use defaults + flags only (for VPS/scripts) */
|
|
17
|
+
yes?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Check if config already exists */
|
|
21
|
+
export function hasConfig(): boolean {
|
|
22
|
+
const globalConfig = resolve(homedir(), ".ppm", "config.yaml");
|
|
23
|
+
const localConfig = resolve(process.cwd(), "ppm.yaml");
|
|
24
|
+
return existsSync(globalConfig) || existsSync(localConfig);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function initProject(options: InitOptions = {}) {
|
|
28
|
+
const nonInteractive = options.yes ?? false;
|
|
29
|
+
|
|
30
|
+
// Check if already initialized
|
|
31
|
+
if (hasConfig() && !nonInteractive) {
|
|
32
|
+
const overwrite = await confirm({
|
|
33
|
+
message: "PPM is already configured. Re-initialize? (this will overwrite your config)",
|
|
34
|
+
default: false,
|
|
35
|
+
});
|
|
36
|
+
if (!overwrite) {
|
|
37
|
+
console.log(" Cancelled.");
|
|
38
|
+
return;
|
|
45
39
|
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log("\n 🔧 PPM Setup\n");
|
|
46
43
|
|
|
47
|
-
|
|
44
|
+
// 1. Port
|
|
45
|
+
const portValue = options.port
|
|
46
|
+
? parseInt(options.port, 10)
|
|
47
|
+
: nonInteractive
|
|
48
|
+
? DEFAULT_PORT
|
|
49
|
+
: parseInt(await input({
|
|
50
|
+
message: "Port:",
|
|
51
|
+
default: String(DEFAULT_PORT),
|
|
52
|
+
validate: (v) => /^\d+$/.test(v) && +v > 0 && +v < 65536 ? true : "Enter valid port (1-65535)",
|
|
53
|
+
}), 10);
|
|
54
|
+
|
|
55
|
+
// 2. Scan directory
|
|
56
|
+
const scanDir = options.scan
|
|
57
|
+
?? (nonInteractive
|
|
58
|
+
? homedir()
|
|
59
|
+
: await input({
|
|
60
|
+
message: "Projects directory to scan:",
|
|
61
|
+
default: homedir(),
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
// 3. Auth
|
|
65
|
+
const authEnabled = options.auth
|
|
66
|
+
?? (nonInteractive
|
|
67
|
+
? true
|
|
68
|
+
: await confirm({
|
|
69
|
+
message: "Enable authentication?",
|
|
70
|
+
default: true,
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
// 4. Password (if auth enabled)
|
|
74
|
+
let authToken = "";
|
|
75
|
+
if (authEnabled) {
|
|
76
|
+
authToken = options.password
|
|
77
|
+
?? (nonInteractive
|
|
78
|
+
? generateToken()
|
|
79
|
+
: await password({
|
|
80
|
+
message: "Set access password (leave empty to auto-generate):",
|
|
81
|
+
}));
|
|
82
|
+
if (!authToken) authToken = generateToken();
|
|
48
83
|
}
|
|
49
84
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
85
|
+
// 5. Share (install cloudflared)
|
|
86
|
+
const wantShare = options.share
|
|
87
|
+
?? (nonInteractive
|
|
88
|
+
? false
|
|
89
|
+
: await confirm({
|
|
90
|
+
message: "Install cloudflared for public sharing (--share)?",
|
|
91
|
+
default: false,
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
// 6. Advanced settings
|
|
95
|
+
let aiModel = "claude-sonnet-4-6";
|
|
96
|
+
let aiEffort: "low" | "medium" | "high" | "max" = "high";
|
|
97
|
+
let aiMaxTurns = 100;
|
|
98
|
+
let aiApiKeyEnv = "ANTHROPIC_API_KEY";
|
|
99
|
+
|
|
100
|
+
if (!nonInteractive) {
|
|
101
|
+
const wantAdvanced = await confirm({
|
|
102
|
+
message: "Configure advanced AI settings?",
|
|
103
|
+
default: false,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (wantAdvanced) {
|
|
107
|
+
aiModel = await select({
|
|
108
|
+
message: "AI model:",
|
|
109
|
+
choices: [
|
|
110
|
+
{ value: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 (fast, recommended)" },
|
|
111
|
+
{ value: "claude-opus-4-6", name: "Claude Opus 4.6 (powerful)" },
|
|
112
|
+
{ value: "claude-haiku-4-5", name: "Claude Haiku 4.5 (cheap)" },
|
|
113
|
+
],
|
|
114
|
+
default: "claude-sonnet-4-6",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
aiEffort = await select({
|
|
118
|
+
message: "Thinking effort:",
|
|
119
|
+
choices: [
|
|
120
|
+
{ value: "low" as const, name: "Low" },
|
|
121
|
+
{ value: "medium" as const, name: "Medium" },
|
|
122
|
+
{ value: "high" as const, name: "High (recommended)" },
|
|
123
|
+
{ value: "max" as const, name: "Max" },
|
|
124
|
+
],
|
|
125
|
+
default: "high" as const,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
aiMaxTurns = parseInt(await input({
|
|
129
|
+
message: "Max turns per chat:",
|
|
130
|
+
default: "100",
|
|
131
|
+
validate: (v) => /^\d+$/.test(v) && +v >= 1 && +v <= 500 ? true : "Enter 1-500",
|
|
132
|
+
}), 10);
|
|
133
|
+
|
|
134
|
+
aiApiKeyEnv = await input({
|
|
135
|
+
message: "API key env variable:",
|
|
136
|
+
default: "ANTHROPIC_API_KEY",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Apply config
|
|
142
|
+
configService.load();
|
|
143
|
+
configService.set("port", portValue);
|
|
144
|
+
configService.set("auth", { enabled: authEnabled, token: authToken });
|
|
145
|
+
configService.set("ai", {
|
|
146
|
+
default_provider: "claude",
|
|
147
|
+
providers: {
|
|
148
|
+
claude: {
|
|
149
|
+
type: "agent-sdk",
|
|
150
|
+
api_key_env: aiApiKeyEnv,
|
|
151
|
+
model: aiModel,
|
|
152
|
+
effort: aiEffort,
|
|
153
|
+
max_turns: aiMaxTurns,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
configService.save();
|
|
158
|
+
|
|
159
|
+
// Scan for projects
|
|
160
|
+
console.log(`\n Scanning ${scanDir} for git repositories...`);
|
|
161
|
+
const repos = projectService.scanForGitRepos(scanDir);
|
|
162
|
+
const existing = configService.get("projects");
|
|
163
|
+
let added = 0;
|
|
164
|
+
|
|
165
|
+
for (const repoPath of repos) {
|
|
166
|
+
const name = repoPath.split("/").pop() ?? "unknown";
|
|
167
|
+
if (existing.some((p) => resolve(p.path) === repoPath || p.name === name)) continue;
|
|
168
|
+
try {
|
|
169
|
+
projectService.add(repoPath, name);
|
|
170
|
+
added++;
|
|
171
|
+
} catch {}
|
|
172
|
+
}
|
|
173
|
+
console.log(` Found ${repos.length} repo(s), added ${added} new project(s).`);
|
|
174
|
+
|
|
175
|
+
// Install cloudflared if requested
|
|
176
|
+
if (wantShare) {
|
|
177
|
+
console.log("\n Installing cloudflared...");
|
|
178
|
+
const { ensureCloudflared } = await import("../../services/cloudflared.service.ts");
|
|
179
|
+
await ensureCloudflared();
|
|
180
|
+
console.log(" ✓ cloudflared ready");
|
|
54
181
|
}
|
|
55
182
|
|
|
56
|
-
|
|
183
|
+
// 8. Next steps
|
|
184
|
+
console.log(`\n ✓ Config saved to ${configService.getConfigPath()}\n`);
|
|
185
|
+
console.log(" Next steps:");
|
|
186
|
+
console.log(` ppm start # Start (daemon, port ${portValue})`);
|
|
187
|
+
console.log(` ppm start -f # Start in foreground`);
|
|
188
|
+
if (wantShare) {
|
|
189
|
+
console.log(` ppm start --share # Start + public URL`);
|
|
190
|
+
}
|
|
191
|
+
if (authEnabled) {
|
|
192
|
+
console.log(`\n Access password: ${authToken}`);
|
|
193
|
+
}
|
|
194
|
+
console.log();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function generateToken(): string {
|
|
198
|
+
const { randomBytes } = require("node:crypto");
|
|
199
|
+
return randomBytes(16).toString("hex");
|
|
57
200
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
const STATUS_FILE = resolve(homedir(), ".ppm", "status.json");
|
|
6
|
+
const PID_FILE = resolve(homedir(), ".ppm", "ppm.pid");
|
|
7
|
+
|
|
8
|
+
interface DaemonStatus {
|
|
9
|
+
running: boolean;
|
|
10
|
+
pid: number | null;
|
|
11
|
+
port: number | null;
|
|
12
|
+
host: string | null;
|
|
13
|
+
shareUrl: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getDaemonStatus(): DaemonStatus {
|
|
17
|
+
const notRunning: DaemonStatus = { running: false, pid: null, port: null, host: null, shareUrl: null };
|
|
18
|
+
|
|
19
|
+
// Try status.json first
|
|
20
|
+
if (existsSync(STATUS_FILE)) {
|
|
21
|
+
try {
|
|
22
|
+
const data = JSON.parse(readFileSync(STATUS_FILE, "utf-8"));
|
|
23
|
+
const pid = data.pid as number;
|
|
24
|
+
// Check if process is actually alive
|
|
25
|
+
try {
|
|
26
|
+
process.kill(pid, 0); // signal 0 = check existence
|
|
27
|
+
return { running: true, pid, port: data.port, host: data.host, shareUrl: data.shareUrl ?? null };
|
|
28
|
+
} catch {
|
|
29
|
+
return notRunning; // stale status file
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
return notRunning;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Fallback to ppm.pid
|
|
37
|
+
if (existsSync(PID_FILE)) {
|
|
38
|
+
try {
|
|
39
|
+
const pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
40
|
+
process.kill(pid, 0);
|
|
41
|
+
return { running: true, pid, port: null, host: null, shareUrl: null };
|
|
42
|
+
} catch {
|
|
43
|
+
return notRunning;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return notRunning;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function showStatus(options: { json?: boolean }) {
|
|
51
|
+
const status = getDaemonStatus();
|
|
52
|
+
|
|
53
|
+
if (options.json) {
|
|
54
|
+
console.log(JSON.stringify(status));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!status.running) {
|
|
59
|
+
console.log(" PPM is not running.");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(`\n PPM daemon is running\n`);
|
|
64
|
+
console.log(` PID: ${status.pid}`);
|
|
65
|
+
if (status.port) console.log(` Local: http://localhost:${status.port}/`);
|
|
66
|
+
if (status.shareUrl) {
|
|
67
|
+
console.log(` Share: ${status.shareUrl}`);
|
|
68
|
+
const qr = await import("qrcode-terminal");
|
|
69
|
+
console.log();
|
|
70
|
+
qr.generate(status.shareUrl, { small: true });
|
|
71
|
+
}
|
|
72
|
+
console.log();
|
|
73
|
+
}
|
package/src/cli/commands/stop.ts
CHANGED
|
@@ -3,29 +3,43 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { readFileSync, unlinkSync, existsSync } from "node:fs";
|
|
4
4
|
|
|
5
5
|
const PID_FILE = resolve(homedir(), ".ppm", "ppm.pid");
|
|
6
|
+
const STATUS_FILE = resolve(homedir(), ".ppm", "status.json");
|
|
6
7
|
|
|
7
8
|
export async function stopServer() {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
let pid: number | null = null;
|
|
10
|
+
|
|
11
|
+
// Try status.json first (new format)
|
|
12
|
+
if (existsSync(STATUS_FILE)) {
|
|
13
|
+
try {
|
|
14
|
+
const status = JSON.parse(readFileSync(STATUS_FILE, "utf-8"));
|
|
15
|
+
pid = status.pid;
|
|
16
|
+
} catch {}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Fallback to ppm.pid (compat)
|
|
20
|
+
if (!pid && existsSync(PID_FILE)) {
|
|
21
|
+
pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
11
22
|
}
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
unlinkSync(
|
|
24
|
+
if (!pid || isNaN(pid)) {
|
|
25
|
+
console.log("No PPM daemon running.");
|
|
26
|
+
// Cleanup stale files
|
|
27
|
+
if (existsSync(STATUS_FILE)) unlinkSync(STATUS_FILE);
|
|
28
|
+
if (existsSync(PID_FILE)) unlinkSync(PID_FILE);
|
|
17
29
|
return;
|
|
18
30
|
}
|
|
19
31
|
|
|
20
32
|
try {
|
|
21
33
|
process.kill(pid);
|
|
22
|
-
unlinkSync(
|
|
34
|
+
if (existsSync(STATUS_FILE)) unlinkSync(STATUS_FILE);
|
|
35
|
+
if (existsSync(PID_FILE)) unlinkSync(PID_FILE);
|
|
23
36
|
console.log(`PPM daemon stopped (PID: ${pid}).`);
|
|
24
37
|
} catch (e) {
|
|
25
38
|
const error = e as NodeJS.ErrnoException;
|
|
26
39
|
if (error.code === "ESRCH") {
|
|
27
|
-
console.log(`Process ${pid} not found. Cleaning up
|
|
28
|
-
unlinkSync(
|
|
40
|
+
console.log(`Process ${pid} not found. Cleaning up stale files.`);
|
|
41
|
+
if (existsSync(STATUS_FILE)) unlinkSync(STATUS_FILE);
|
|
42
|
+
if (existsSync(PID_FILE)) unlinkSync(PID_FILE);
|
|
29
43
|
} else {
|
|
30
44
|
console.error(`Failed to stop process ${pid}: ${error.message}`);
|
|
31
45
|
}
|
package/src/index.ts
CHANGED
|
@@ -10,11 +10,18 @@ program
|
|
|
10
10
|
|
|
11
11
|
program
|
|
12
12
|
.command("start")
|
|
13
|
-
.description("Start the PPM server")
|
|
13
|
+
.description("Start the PPM server (background by default)")
|
|
14
14
|
.option("-p, --port <port>", "Port to listen on")
|
|
15
|
-
.option("-
|
|
15
|
+
.option("-f, --foreground", "Run in foreground (default: background daemon)")
|
|
16
|
+
.option("-d, --daemon", "Run as background daemon (default, kept for compat)")
|
|
17
|
+
.option("-s, --share", "Share via public URL (Cloudflare tunnel)")
|
|
16
18
|
.option("-c, --config <path>", "Path to config file")
|
|
17
19
|
.action(async (options) => {
|
|
20
|
+
// Auto-init on first run
|
|
21
|
+
const { hasConfig, initProject } = await import("./cli/commands/init.ts");
|
|
22
|
+
if (!hasConfig()) {
|
|
23
|
+
await initProject();
|
|
24
|
+
}
|
|
18
25
|
const { startServer } = await import("./server/index.ts");
|
|
19
26
|
await startServer(options);
|
|
20
27
|
});
|
|
@@ -27,6 +34,15 @@ program
|
|
|
27
34
|
await stopServer();
|
|
28
35
|
});
|
|
29
36
|
|
|
37
|
+
program
|
|
38
|
+
.command("status")
|
|
39
|
+
.description("Show PPM daemon status")
|
|
40
|
+
.option("--json", "Output as JSON")
|
|
41
|
+
.action(async (options) => {
|
|
42
|
+
const { showStatus } = await import("./cli/commands/status.ts");
|
|
43
|
+
await showStatus(options);
|
|
44
|
+
});
|
|
45
|
+
|
|
30
46
|
program
|
|
31
47
|
.command("open")
|
|
32
48
|
.description("Open PPM in browser")
|
|
@@ -38,10 +54,17 @@ program
|
|
|
38
54
|
|
|
39
55
|
program
|
|
40
56
|
.command("init")
|
|
41
|
-
.description("Initialize PPM configuration
|
|
42
|
-
.
|
|
57
|
+
.description("Initialize PPM configuration (interactive or via flags)")
|
|
58
|
+
.option("-p, --port <port>", "Port to listen on")
|
|
59
|
+
.option("--scan <path>", "Directory to scan for git repos")
|
|
60
|
+
.option("--auth", "Enable authentication")
|
|
61
|
+
.option("--no-auth", "Disable authentication")
|
|
62
|
+
.option("--password <pw>", "Set access password")
|
|
63
|
+
.option("--share", "Pre-install cloudflared for sharing")
|
|
64
|
+
.option("-y, --yes", "Non-interactive mode (use defaults + flags)")
|
|
65
|
+
.action(async (options) => {
|
|
43
66
|
const { initProject } = await import("./cli/commands/init.ts");
|
|
44
|
-
await initProject();
|
|
67
|
+
await initProject(options);
|
|
45
68
|
});
|
|
46
69
|
|
|
47
70
|
const { registerProjectsCommands } = await import("./cli/commands/projects.ts");
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
ChatMessage,
|
|
13
13
|
UsageInfo,
|
|
14
14
|
} from "./provider.interface.ts";
|
|
15
|
+
import { configService } from "../services/config.service.ts";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Pending approval: canUseTool callback creates a promise,
|
|
@@ -39,6 +40,13 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
39
40
|
/** Latest known usage/rate-limit info (shared across all sessions) */
|
|
40
41
|
private latestUsage: UsageInfo = {};
|
|
41
42
|
|
|
43
|
+
/** Read current provider config from yaml (fresh each call) */
|
|
44
|
+
private getProviderConfig(): Partial<import("../types/config.ts").AIProviderConfig> {
|
|
45
|
+
const ai = configService.get("ai");
|
|
46
|
+
const providerId = ai.default_provider ?? "claude";
|
|
47
|
+
return ai.providers[providerId] ?? {};
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
async createSession(config: SessionConfig): Promise<Session> {
|
|
43
51
|
const id = crypto.randomUUID();
|
|
44
52
|
const meta: Session = {
|
|
@@ -176,9 +184,17 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
176
184
|
|
|
177
185
|
const requestId = crypto.randomUUID();
|
|
178
186
|
|
|
187
|
+
const APPROVAL_TIMEOUT_MS = 60_000;
|
|
179
188
|
const approvalPromise = new Promise<{ approved: boolean; data?: unknown }>(
|
|
180
189
|
(resolve) => {
|
|
181
190
|
this.pendingApprovals.set(requestId, { resolve });
|
|
191
|
+
// Auto-deny after timeout if FE doesn't respond
|
|
192
|
+
setTimeout(() => {
|
|
193
|
+
if (this.pendingApprovals.has(requestId)) {
|
|
194
|
+
this.pendingApprovals.delete(requestId);
|
|
195
|
+
resolve({ approved: false });
|
|
196
|
+
}
|
|
197
|
+
}, APPROVAL_TIMEOUT_MS);
|
|
182
198
|
},
|
|
183
199
|
);
|
|
184
200
|
|
|
@@ -191,7 +207,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
191
207
|
});
|
|
192
208
|
approvalNotify?.();
|
|
193
209
|
|
|
194
|
-
// Wait for FE to send back answers
|
|
210
|
+
// Wait for FE to send back answers (or timeout)
|
|
195
211
|
const result = await approvalPromise;
|
|
196
212
|
|
|
197
213
|
if (result.approved && result.data) {
|
|
@@ -204,21 +220,46 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
204
220
|
};
|
|
205
221
|
|
|
206
222
|
let assistantContent = "";
|
|
223
|
+
let resultSubtype: string | undefined;
|
|
224
|
+
let resultNumTurns: number | undefined;
|
|
207
225
|
|
|
208
226
|
try {
|
|
227
|
+
const providerConfig = this.getProviderConfig();
|
|
228
|
+
|
|
209
229
|
const q = query({
|
|
210
230
|
prompt: message,
|
|
211
231
|
options: {
|
|
212
232
|
sessionId: isFirstMessage ? sessionId : undefined,
|
|
213
233
|
resume: isFirstMessage ? undefined : sessionId,
|
|
214
234
|
cwd: meta.projectPath,
|
|
215
|
-
|
|
235
|
+
// Use full Claude Code system prompt (coding guidelines, security, response style)
|
|
236
|
+
systemPrompt: { type: "preset", preset: "claude_code" },
|
|
237
|
+
// Load project CLAUDE.md, skills, and hooks from project directory
|
|
238
|
+
settingSources: ["project"],
|
|
239
|
+
// Neutralize Anthropic env vars so SDK uses subscription, not project .env keys.
|
|
240
|
+
env: {
|
|
241
|
+
...process.env,
|
|
242
|
+
ANTHROPIC_API_KEY: "",
|
|
243
|
+
ANTHROPIC_BASE_URL: "",
|
|
244
|
+
ANTHROPIC_AUTH_TOKEN: "",
|
|
245
|
+
},
|
|
246
|
+
// Override project-local Claude settings that may restrict tool permissions
|
|
247
|
+
settings: { permissions: { allow: [], deny: [] } },
|
|
216
248
|
allowedTools: [
|
|
217
249
|
"Read", "Write", "Edit", "Bash", "Glob", "Grep",
|
|
218
250
|
"WebSearch", "WebFetch", "AskUserQuestion",
|
|
251
|
+
"Agent", "Skill", "TodoWrite", "ToolSearch",
|
|
219
252
|
],
|
|
220
253
|
permissionMode: "bypassPermissions",
|
|
221
254
|
allowDangerouslySkipPermissions: true,
|
|
255
|
+
// Config-driven values from ppm.yaml
|
|
256
|
+
...(providerConfig.model && { model: providerConfig.model }),
|
|
257
|
+
...(providerConfig.effort && { effort: providerConfig.effort }),
|
|
258
|
+
maxTurns: providerConfig.max_turns ?? 100,
|
|
259
|
+
...(providerConfig.max_budget_usd && { maxBudgetUsd: providerConfig.max_budget_usd }),
|
|
260
|
+
...(providerConfig.thinking_budget_tokens != null && {
|
|
261
|
+
thinkingBudgetTokens: providerConfig.thinking_budget_tokens,
|
|
262
|
+
}),
|
|
222
263
|
canUseTool,
|
|
223
264
|
includePartialMessages: true,
|
|
224
265
|
} as any,
|
|
@@ -232,11 +273,27 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
232
273
|
let pendingToolCount = 0;
|
|
233
274
|
|
|
234
275
|
for await (const msg of q) {
|
|
276
|
+
// Debug: log all SDK events to understand flow
|
|
277
|
+
|
|
235
278
|
// Yield any queued approval events
|
|
236
279
|
while (approvalEvents.length > 0) {
|
|
237
280
|
yield approvalEvents.shift()!;
|
|
238
281
|
}
|
|
239
282
|
|
|
283
|
+
// Capture SDK session metadata from init message
|
|
284
|
+
if (msg.type === "system" && (msg as any).subtype === "init") {
|
|
285
|
+
const initMsg = msg as any;
|
|
286
|
+
// SDK may assign a different session_id than our UUID
|
|
287
|
+
if (initMsg.session_id && initMsg.session_id !== sessionId) {
|
|
288
|
+
// Update our mapping so resume works with SDK's actual ID
|
|
289
|
+
const oldMeta = this.activeSessions.get(sessionId);
|
|
290
|
+
if (oldMeta) {
|
|
291
|
+
this.activeSessions.set(initMsg.session_id, { ...oldMeta, id: initMsg.session_id });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
240
297
|
// When tools were pending and a new assistant/stream_event arrives,
|
|
241
298
|
// the SDK has finished executing tools. Fetch tool_results from session history.
|
|
242
299
|
if (pendingToolCount > 0 && (msg.type === "assistant" || (msg as any).type === "partial" || (msg as any).type === "stream_event")) {
|
|
@@ -369,12 +426,31 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
369
426
|
}
|
|
370
427
|
|
|
371
428
|
const result = msg as any;
|
|
429
|
+
const subtype = result.subtype as string | undefined;
|
|
430
|
+
|
|
372
431
|
// Yield final cost + any rate limit info from result
|
|
373
432
|
const usage: Record<string, unknown> = {};
|
|
374
433
|
if (result.total_cost_usd != null) usage.totalCostUsd = result.total_cost_usd;
|
|
375
434
|
if (Object.keys(usage).length > 0) {
|
|
376
435
|
yield { type: "usage", usage };
|
|
377
436
|
}
|
|
437
|
+
|
|
438
|
+
// Surface non-success subtypes as errors so FE can display them
|
|
439
|
+
if (subtype && subtype !== "success") {
|
|
440
|
+
const errorMessages: Record<string, string> = {
|
|
441
|
+
error_max_turns: "Agent reached maximum turn limit.",
|
|
442
|
+
error_max_budget_usd: "Agent reached budget limit.",
|
|
443
|
+
error_during_execution: "Agent encountered an error during execution.",
|
|
444
|
+
};
|
|
445
|
+
yield {
|
|
446
|
+
type: "error",
|
|
447
|
+
message: errorMessages[subtype] ?? `Agent stopped: ${subtype}`,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Store subtype and numTurns for the done event
|
|
452
|
+
resultSubtype = subtype;
|
|
453
|
+
resultNumTurns = result.num_turns as number | undefined;
|
|
378
454
|
break;
|
|
379
455
|
}
|
|
380
456
|
}
|
|
@@ -393,7 +469,12 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
393
469
|
this.activeQueries.delete(sessionId);
|
|
394
470
|
}
|
|
395
471
|
|
|
396
|
-
yield {
|
|
472
|
+
yield {
|
|
473
|
+
type: "done",
|
|
474
|
+
sessionId,
|
|
475
|
+
resultSubtype: resultSubtype as any,
|
|
476
|
+
numTurns: resultNumTurns,
|
|
477
|
+
};
|
|
397
478
|
}
|
|
398
479
|
|
|
399
480
|
/** Get latest cached usage/rate-limit info */
|