@hienlh/ppm 0.9.62 → 0.9.64
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 +7 -0
- package/package.json +2 -1
- package/scripts/generate-bot-coordinator.ts +397 -0
- package/src/cli/commands/cloud.ts +44 -8
- package/src/services/ppmbot/cli-reference-default.ts +330 -0
- package/src/services/ppmbot/ppmbot-service.ts +8 -1
- package/src/services/ppmbot/ppmbot-session.ts +93 -26
- package/src/services/supervisor.ts +44 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.63] - 2026-04-08
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Cloud auto-link**: `ppm cloud login` now auto-links device (no separate `ppm cloud link` needed). `ppm cloud logout` auto-unlinks.
|
|
7
|
+
- **Supervisor cloud monitor**: Supervisor periodically checks cloud-device.json and auto-connects/disconnects/reconnects WS as needed. Fixes "stuck offline" after upgrade or file loss.
|
|
8
|
+
- **Stale tests**: Aligned usage-cache and chat-routes tests with current API response shapes.
|
|
9
|
+
|
|
3
10
|
## [0.9.62] - 2026-04-08
|
|
4
11
|
|
|
5
12
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hienlh/ppm",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.64",
|
|
4
4
|
"description": "Personal Project Manager — mobile-first web IDE with AI assistance",
|
|
5
5
|
"author": "hienlh",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"build": "bun run build:web && bun build src/index.ts --compile --outfile dist/ppm",
|
|
19
19
|
"start": "bun run src/index.ts start",
|
|
20
20
|
"typecheck": "bunx tsc --noEmit",
|
|
21
|
+
"generate:bot": "bun scripts/generate-bot-coordinator.ts --update",
|
|
21
22
|
"prepublishOnly": "bun run build:web"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Auto-generate PPMBot coordinator identity from Commander.js CLI source.
|
|
4
|
+
*
|
|
5
|
+
* Parses src/index.ts + src/cli/commands/*.ts to extract all commands,
|
|
6
|
+
* descriptions, options, and arguments. Produces coordinator.md content
|
|
7
|
+
* with full CLI reference, decision framework, and safety rules.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* bun scripts/generate-bot-coordinator.ts # print to stdout
|
|
11
|
+
* bun scripts/generate-bot-coordinator.ts --update # write coordinator.md + update source
|
|
12
|
+
* bun scripts/generate-bot-coordinator.ts --write-md # only write ~/.ppm/bot/coordinator.md
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
16
|
+
import { join, resolve } from "node:path";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
|
|
19
|
+
const ROOT = resolve(import.meta.dir, "..");
|
|
20
|
+
const INDEX_PATH = join(ROOT, "src", "index.ts");
|
|
21
|
+
const CMD_DIR = join(ROOT, "src", "cli", "commands");
|
|
22
|
+
|
|
23
|
+
// ── Types ──────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
interface CliOption {
|
|
26
|
+
flags: string;
|
|
27
|
+
description: string;
|
|
28
|
+
defaultValue?: string;
|
|
29
|
+
required?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface CliCommand {
|
|
33
|
+
/** Full display name including parent path, e.g. "branch create" */
|
|
34
|
+
displayName: string;
|
|
35
|
+
description: string;
|
|
36
|
+
args: string[];
|
|
37
|
+
options: CliOption[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface CommandGroup {
|
|
41
|
+
name: string;
|
|
42
|
+
description: string;
|
|
43
|
+
commands: CliCommand[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Parsing ────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/** Extract first quoted string from a regex match, handling mixed quote types */
|
|
49
|
+
function extractQuoted(line: string, after: string): string | null {
|
|
50
|
+
const idx = line.indexOf(after);
|
|
51
|
+
if (idx === -1) return null;
|
|
52
|
+
const rest = line.slice(idx + after.length);
|
|
53
|
+
|
|
54
|
+
// Match opening quote, then capture until same closing quote
|
|
55
|
+
const m = rest.match(/["'`]((?:[^"'`\\]|\\.)*)["'`]/);
|
|
56
|
+
return m ? m[1]! : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Extract string argument from .description("...") — handles embedded quotes */
|
|
60
|
+
function extractDescription(line: string): string | null {
|
|
61
|
+
// Try specific patterns: .description("..."), .description('...')
|
|
62
|
+
const dblMatch = line.match(/\.description\(\s*"([^"]*)"\s*\)/);
|
|
63
|
+
if (dblMatch) return dblMatch[1]!;
|
|
64
|
+
|
|
65
|
+
const sglMatch = line.match(/\.description\(\s*'([^']*)'\s*\)/);
|
|
66
|
+
if (sglMatch) return sglMatch[1]!;
|
|
67
|
+
|
|
68
|
+
const btMatch = line.match(/\.description\(\s*`([^`]*)`\s*\)/);
|
|
69
|
+
if (btMatch) return btMatch[1]!;
|
|
70
|
+
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse a Commander.js source file into commands.
|
|
76
|
+
* Tracks variable assignments to resolve nested groups:
|
|
77
|
+
* const branch = git.command("branch") → branch.command("create") is "branch create"
|
|
78
|
+
*
|
|
79
|
+
* Handles multi-line chaining where receiver is on previous line:
|
|
80
|
+
* branch
|
|
81
|
+
* .command("create <name>")
|
|
82
|
+
*/
|
|
83
|
+
function parseFile(filePath: string): CliCommand[] {
|
|
84
|
+
const src = readFileSync(filePath, "utf-8");
|
|
85
|
+
const lines = src.split("\n");
|
|
86
|
+
const commands: CliCommand[] = [];
|
|
87
|
+
|
|
88
|
+
// Track variable → parent path mapping
|
|
89
|
+
// e.g. "git" → "", "branch" → "branch", "mem" → "memory"
|
|
90
|
+
const varParent = new Map<string, string>();
|
|
91
|
+
|
|
92
|
+
// Track function parameters as potential receivers: function xxx(param: Command)
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
const funcParamMatch = line.match(
|
|
95
|
+
/function\s+\w+\(\s*(\w+)\s*:\s*Command\s*\)/,
|
|
96
|
+
);
|
|
97
|
+
if (funcParamMatch) {
|
|
98
|
+
varParent.set(funcParamMatch[1]!, "");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Last standalone variable seen (for multi-line chaining: `branch\n .command(...)`) */
|
|
103
|
+
let lastStandaloneVar = "";
|
|
104
|
+
|
|
105
|
+
let currentCmd: {
|
|
106
|
+
displayName: string;
|
|
107
|
+
description: string;
|
|
108
|
+
args: string[];
|
|
109
|
+
options: CliOption[];
|
|
110
|
+
} | null = null;
|
|
111
|
+
|
|
112
|
+
for (let i = 0; i < lines.length; i++) {
|
|
113
|
+
const line = lines[i]!;
|
|
114
|
+
|
|
115
|
+
// Track standalone variable references (for multi-line chaining)
|
|
116
|
+
const standaloneMatch = line.match(/^\s+(\w+)\s*$/);
|
|
117
|
+
if (standaloneMatch) {
|
|
118
|
+
lastStandaloneVar = standaloneMatch[1]!;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Detect variable assignment: const/let varName = something.command("name")
|
|
122
|
+
const assignMatch = line.match(
|
|
123
|
+
/(?:const|let)\s+(\w+)\s*=\s*(\w+)\.command\(\s*["'`](\w+)["'`]\s*\)/,
|
|
124
|
+
);
|
|
125
|
+
if (assignMatch) {
|
|
126
|
+
const varName = assignMatch[1]!;
|
|
127
|
+
const receiver = assignMatch[2]!;
|
|
128
|
+
const cmdName = assignMatch[3]!;
|
|
129
|
+
|
|
130
|
+
// If this creates a known CLI group (git, bot, etc.), its path stays ""
|
|
131
|
+
// because formatCommand already prepends "ppm <group>"
|
|
132
|
+
if (cmdName in GROUP_MAP) {
|
|
133
|
+
varParent.set(varName, "");
|
|
134
|
+
} else {
|
|
135
|
+
const parentPath = varParent.get(receiver);
|
|
136
|
+
if (parentPath !== undefined) {
|
|
137
|
+
varParent.set(varName, parentPath ? `${parentPath} ${cmdName}` : cmdName);
|
|
138
|
+
} else {
|
|
139
|
+
varParent.set(varName, "");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Detect chained .command("name") — not an assignment
|
|
146
|
+
const cmdMatch = line.match(/\.command\(\s*["'`]([^"'`]+)["'`]\s*\)/);
|
|
147
|
+
if (cmdMatch) {
|
|
148
|
+
// Save previous command
|
|
149
|
+
if (currentCmd) {
|
|
150
|
+
commands.push({ ...currentCmd });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const fullArg = cmdMatch[1]!;
|
|
154
|
+
const parts = fullArg.split(/\s+/);
|
|
155
|
+
const name = parts[0]!;
|
|
156
|
+
const args = parts.slice(1);
|
|
157
|
+
|
|
158
|
+
// Determine receiver: same line or previous line (multi-line chaining)
|
|
159
|
+
const sameLineReceiver = line.match(/(\w+)\.command\(/);
|
|
160
|
+
let receiver = sameLineReceiver ? sameLineReceiver[1]! : "";
|
|
161
|
+
|
|
162
|
+
// If receiver looks like a keyword (not a variable), try lastStandaloneVar
|
|
163
|
+
if (!receiver || receiver === "command") {
|
|
164
|
+
// .command() at start of line → look at previous non-empty line
|
|
165
|
+
receiver = lastStandaloneVar;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const parentPath = varParent.get(receiver) ?? "";
|
|
169
|
+
const displayName = parentPath ? `${parentPath} ${name}` : name;
|
|
170
|
+
|
|
171
|
+
currentCmd = { displayName, description: "", args, options: [] };
|
|
172
|
+
lastStandaloneVar = "";
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!currentCmd) continue;
|
|
177
|
+
|
|
178
|
+
// Description
|
|
179
|
+
const desc = extractDescription(line);
|
|
180
|
+
if (desc !== null && line.includes(".description(")) {
|
|
181
|
+
currentCmd.description = desc;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Option: .option("flags", "desc", "default?")
|
|
185
|
+
const optMatch = line.match(
|
|
186
|
+
/\.option\(\s*["'`]([^"'`]+)["'`]\s*,\s*["'`]([^"'`]+)["'`](?:\s*,\s*["'`]([^"'`]+)["'`])?\s*\)/,
|
|
187
|
+
);
|
|
188
|
+
if (optMatch) {
|
|
189
|
+
currentCmd.options.push({
|
|
190
|
+
flags: optMatch[1]!,
|
|
191
|
+
description: optMatch[2]!,
|
|
192
|
+
defaultValue: optMatch[3],
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Required option
|
|
197
|
+
const reqMatch = line.match(
|
|
198
|
+
/\.requiredOption\(\s*["'`]([^"'`]+)["'`]\s*,\s*["'`]([^"'`]+)["'`](?:\s*,\s*["'`]([^"'`]+)["'`])?\s*\)/,
|
|
199
|
+
);
|
|
200
|
+
if (reqMatch) {
|
|
201
|
+
currentCmd.options.push({
|
|
202
|
+
flags: reqMatch[1]!,
|
|
203
|
+
description: reqMatch[2]!,
|
|
204
|
+
defaultValue: reqMatch[3],
|
|
205
|
+
required: true,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Argument: .argument("name")
|
|
210
|
+
const argMatch = line.match(/\.argument\(\s*["'`]([^"'`]+)["'`]/);
|
|
211
|
+
if (argMatch) {
|
|
212
|
+
currentCmd.args.push(argMatch[1]!);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Push last command
|
|
217
|
+
if (currentCmd) {
|
|
218
|
+
commands.push({ ...currentCmd });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return commands;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Known command groups with their source files */
|
|
225
|
+
const GROUP_MAP: Record<string, { description: string; file: string }> = {
|
|
226
|
+
projects: { description: "Manage registered projects", file: "projects.ts" },
|
|
227
|
+
config: { description: "Configuration management", file: "config-cmd.ts" },
|
|
228
|
+
git: { description: "Git operations for a project", file: "git-cmd.ts" },
|
|
229
|
+
chat: { description: "AI chat sessions", file: "chat-cmd.ts" },
|
|
230
|
+
db: { description: "Database connections & queries", file: "db-cmd.ts" },
|
|
231
|
+
autostart: { description: "Auto-start on boot", file: "autostart.ts" },
|
|
232
|
+
cloud: { description: "PPM Cloud — device registry + tunnel", file: "cloud.ts" },
|
|
233
|
+
ext: { description: "Manage PPM extensions", file: "ext-cmd.ts" },
|
|
234
|
+
bot: { description: "PPMBot coordinator utilities", file: "bot-cmd.ts" },
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
function buildGroups(): CommandGroup[] {
|
|
238
|
+
const groups: CommandGroup[] = [];
|
|
239
|
+
|
|
240
|
+
// Top-level commands from index.ts
|
|
241
|
+
const indexCmds = parseFile(INDEX_PATH);
|
|
242
|
+
const groupNames = new Set(Object.keys(GROUP_MAP));
|
|
243
|
+
const topLevel = indexCmds.filter((c) => !groupNames.has(c.displayName));
|
|
244
|
+
|
|
245
|
+
if (topLevel.length > 0) {
|
|
246
|
+
groups.push({
|
|
247
|
+
name: "core",
|
|
248
|
+
description: "Server & system management",
|
|
249
|
+
commands: topLevel,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Sub-command groups from individual files
|
|
254
|
+
for (const [groupName, info] of Object.entries(GROUP_MAP)) {
|
|
255
|
+
const filePath = join(CMD_DIR, info.file);
|
|
256
|
+
if (!existsSync(filePath)) continue;
|
|
257
|
+
|
|
258
|
+
const cmds = parseFile(filePath);
|
|
259
|
+
|
|
260
|
+
// Filter out the group parent command and pure sub-group declarations
|
|
261
|
+
// (e.g. "branch" with no description = just a group container)
|
|
262
|
+
const subCmds = cmds.filter((c) => {
|
|
263
|
+
if (c.displayName === groupName) return false;
|
|
264
|
+
// Skip pure group containers (no description, no args)
|
|
265
|
+
if (!c.description && c.args.length === 0 && c.options.length === 0) return false;
|
|
266
|
+
return true;
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
groups.push({
|
|
270
|
+
name: groupName,
|
|
271
|
+
description: info.description,
|
|
272
|
+
commands: subCmds,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return groups;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── Output Generation ──────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
function formatOption(opt: CliOption): string {
|
|
282
|
+
const req = opt.required ? " (required)" : "";
|
|
283
|
+
const def = opt.defaultValue ? ` [default: ${opt.defaultValue}]` : "";
|
|
284
|
+
return ` ${opt.flags} — ${opt.description}${req}${def}`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function formatCommand(group: string, cmd: CliCommand): string {
|
|
288
|
+
const args = cmd.args.length > 0 ? " " + cmd.args.join(" ") : "";
|
|
289
|
+
const prefix = group === "core" ? "ppm" : `ppm ${group}`;
|
|
290
|
+
let line = `${prefix} ${cmd.displayName}${args}`;
|
|
291
|
+
if (cmd.description) line += `\n ${cmd.description}`;
|
|
292
|
+
if (cmd.options.length > 0) {
|
|
293
|
+
line += "\n" + cmd.options.map(formatOption).join("\n");
|
|
294
|
+
}
|
|
295
|
+
return line;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Generate CLI-only reference (for cli-reference.md) */
|
|
299
|
+
function generateCliReference(groups: CommandGroup[]): string {
|
|
300
|
+
const sections: string[] = [];
|
|
301
|
+
sections.push(`# PPM CLI Reference`);
|
|
302
|
+
|
|
303
|
+
for (const group of groups) {
|
|
304
|
+
const heading = group.name === "core"
|
|
305
|
+
? `## Core Commands (${group.description})`
|
|
306
|
+
: `## ppm ${group.name} — ${group.description}`;
|
|
307
|
+
sections.push(heading);
|
|
308
|
+
|
|
309
|
+
const cmdLines = group.commands.map((c) => formatCommand(group.name, c));
|
|
310
|
+
sections.push("```");
|
|
311
|
+
sections.push(cmdLines.join("\n\n"));
|
|
312
|
+
sections.push("```");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
sections.push(`
|
|
316
|
+
## Quick Reference — Task Delegation
|
|
317
|
+
\`\`\`
|
|
318
|
+
ppm bot delegate --chat <chatId> --project <name> --prompt "<enriched task>"
|
|
319
|
+
ppm bot task-status <id>
|
|
320
|
+
ppm bot task-result <id>
|
|
321
|
+
ppm bot tasks
|
|
322
|
+
\`\`\`
|
|
323
|
+
|
|
324
|
+
## Quick Reference — Memory
|
|
325
|
+
\`\`\`
|
|
326
|
+
ppm bot memory save "<content>" -c <category>
|
|
327
|
+
ppm bot memory list
|
|
328
|
+
ppm bot memory forget "<topic>"
|
|
329
|
+
\`\`\`
|
|
330
|
+
|
|
331
|
+
## Tips
|
|
332
|
+
- Use \`--json\` flag when parsing command output programmatically
|
|
333
|
+
- For git/chat/db operations: always specify \`--project <name>\` or connection name`);
|
|
334
|
+
|
|
335
|
+
return sections.join("\n");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ── Source Code Update ────────────────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
const CLI_DEFAULT_PATH = join(ROOT, "src", "services", "ppmbot", "cli-reference-default.ts");
|
|
341
|
+
|
|
342
|
+
function updateBundledDefault(cliRef: string): void {
|
|
343
|
+
const escaped = cliRef
|
|
344
|
+
.replace(/\\/g, "\\\\")
|
|
345
|
+
.replace(/`/g, "\\`")
|
|
346
|
+
.replace(/\$\{/g, "\\${");
|
|
347
|
+
|
|
348
|
+
const src = readFileSync(CLI_DEFAULT_PATH, "utf-8");
|
|
349
|
+
const startMarker = "export const DEFAULT_CLI_REFERENCE = `";
|
|
350
|
+
const startIdx = src.indexOf(startMarker);
|
|
351
|
+
if (startIdx === -1) {
|
|
352
|
+
console.error("Could not find DEFAULT_CLI_REFERENCE in cli-reference-default.ts");
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const afterStart = startIdx + startMarker.length;
|
|
357
|
+
const endIdx = src.indexOf("\n`;\n", afterStart);
|
|
358
|
+
if (endIdx === -1) {
|
|
359
|
+
console.error("Could not find closing backtick for DEFAULT_CLI_REFERENCE");
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const updated = src.slice(0, afterStart) + escaped + src.slice(endIdx);
|
|
364
|
+
writeFileSync(CLI_DEFAULT_PATH, updated);
|
|
365
|
+
console.log(`Updated: ${CLI_DEFAULT_PATH}`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function writeCliReferenceMd(cliRef: string): void {
|
|
369
|
+
const dir = join(homedir(), ".ppm", "bot");
|
|
370
|
+
mkdirSync(dir, { recursive: true });
|
|
371
|
+
const cliRefPath = join(dir, "cli-reference.md");
|
|
372
|
+
// Read version from package.json
|
|
373
|
+
const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8"));
|
|
374
|
+
writeFileSync(cliRefPath, `<!-- ppm-version: ${pkg.version} -->\n${cliRef}`);
|
|
375
|
+
console.log(`Written: ${cliRefPath}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ── Main ───────────────────────────────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
const cliArgs = process.argv.slice(2);
|
|
381
|
+
const groups = buildGroups();
|
|
382
|
+
const cliRef = generateCliReference(groups);
|
|
383
|
+
|
|
384
|
+
if (cliArgs.includes("--cli-only")) {
|
|
385
|
+
// Runtime mode: just output CLI reference to stdout (used by ensureCliReference)
|
|
386
|
+
console.log(cliRef);
|
|
387
|
+
} else if (cliArgs.includes("--update")) {
|
|
388
|
+
// Dev mode: write cli-reference.md + update bundled default
|
|
389
|
+
writeCliReferenceMd(cliRef);
|
|
390
|
+
updateBundledDefault(cliRef);
|
|
391
|
+
console.log("\nDone. Review the changes and test with PPMBot.");
|
|
392
|
+
} else if (cliArgs.includes("--write-md")) {
|
|
393
|
+
writeCliReferenceMd(cliRef);
|
|
394
|
+
} else {
|
|
395
|
+
// Default: stdout
|
|
396
|
+
console.log(cliRef);
|
|
397
|
+
}
|
|
@@ -28,7 +28,21 @@ export function registerCloudCommands(program: Command): void {
|
|
|
28
28
|
const existing = getCloudAuth();
|
|
29
29
|
if (existing) {
|
|
30
30
|
console.log(` Already logged in as ${existing.email}`);
|
|
31
|
-
|
|
31
|
+
|
|
32
|
+
// Auto-link if not yet linked
|
|
33
|
+
const { getCloudDevice, linkDevice } = await import("../../services/cloud.service.ts");
|
|
34
|
+
if (!getCloudDevice()) {
|
|
35
|
+
try {
|
|
36
|
+
const device = await linkDevice();
|
|
37
|
+
console.log(` ✓ Machine linked: ${device.name}\n`);
|
|
38
|
+
} catch (linkErr: unknown) {
|
|
39
|
+
const linkMsg = linkErr instanceof Error ? linkErr.message : String(linkErr);
|
|
40
|
+
console.warn(` ⚠ Auto-link failed: ${linkMsg}`);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
console.log(` Run 'ppm cloud logout' to switch accounts.`);
|
|
44
|
+
}
|
|
45
|
+
console.log();
|
|
32
46
|
return;
|
|
33
47
|
}
|
|
34
48
|
|
|
@@ -51,8 +65,19 @@ export function registerCloudCommands(program: Command): void {
|
|
|
51
65
|
}
|
|
52
66
|
}
|
|
53
67
|
|
|
54
|
-
console.log(`\n ✓ Logged in as ${auth.email}
|
|
55
|
-
|
|
68
|
+
console.log(`\n ✓ Logged in as ${auth.email}`);
|
|
69
|
+
|
|
70
|
+
// Auto-link device after login
|
|
71
|
+
try {
|
|
72
|
+
const { linkDevice } = await import("../../services/cloud.service.ts");
|
|
73
|
+
const device = await linkDevice();
|
|
74
|
+
console.log(` ✓ Machine linked: ${device.name}`);
|
|
75
|
+
} catch (linkErr: unknown) {
|
|
76
|
+
const linkMsg = linkErr instanceof Error ? linkErr.message : String(linkErr);
|
|
77
|
+
console.warn(` ⚠ Auto-link failed: ${linkMsg}`);
|
|
78
|
+
console.log(` Run 'ppm cloud link' manually to register this machine.`);
|
|
79
|
+
}
|
|
80
|
+
console.log();
|
|
56
81
|
} catch (err: unknown) {
|
|
57
82
|
const msg = err instanceof Error ? err.message : String(err);
|
|
58
83
|
console.error(` ✗ Login failed: ${msg}\n`);
|
|
@@ -64,17 +89,28 @@ export function registerCloudCommands(program: Command): void {
|
|
|
64
89
|
.command("logout")
|
|
65
90
|
.description("Sign out from PPM Cloud")
|
|
66
91
|
.action(async () => {
|
|
67
|
-
const { removeCloudAuth, getCloudAuth } = await import(
|
|
92
|
+
const { removeCloudAuth, getCloudAuth, unlinkDevice, getCloudDevice } = await import(
|
|
68
93
|
"../../services/cloud.service.ts"
|
|
69
94
|
);
|
|
70
95
|
|
|
71
96
|
const auth = getCloudAuth();
|
|
72
|
-
|
|
73
|
-
if (auth) {
|
|
74
|
-
console.log(` ✓ Logged out (was: ${auth.email})\n`);
|
|
75
|
-
} else {
|
|
97
|
+
if (!auth) {
|
|
76
98
|
console.log(` Not logged in.\n`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Auto-unlink device before removing auth
|
|
103
|
+
if (getCloudDevice()) {
|
|
104
|
+
try {
|
|
105
|
+
await unlinkDevice();
|
|
106
|
+
console.log(` ✓ Machine unlinked`);
|
|
107
|
+
} catch {
|
|
108
|
+
// Non-blocking — still logout even if unlink fails
|
|
109
|
+
}
|
|
77
110
|
}
|
|
111
|
+
|
|
112
|
+
removeCloudAuth();
|
|
113
|
+
console.log(` ✓ Logged out (was: ${auth.email})\n`);
|
|
78
114
|
});
|
|
79
115
|
|
|
80
116
|
cmd
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundled CLI reference fallback for compiled binary (when generator script is unavailable).
|
|
3
|
+
* Auto-generated by: bun scripts/generate-bot-coordinator.ts --update-default
|
|
4
|
+
*
|
|
5
|
+
* To regenerate: bun generate:bot
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// prettier-ignore
|
|
9
|
+
export const DEFAULT_CLI_REFERENCE = `# PPM CLI Reference
|
|
10
|
+
## Core Commands (Server & system management)
|
|
11
|
+
\`\`\`
|
|
12
|
+
ppm start
|
|
13
|
+
Start the PPM server (background by default)
|
|
14
|
+
-p, --port <port> — Port to listen on
|
|
15
|
+
-s, --share — (deprecated) Tunnel is now always enabled
|
|
16
|
+
-c, --config <path> — Path to config file (YAML import into DB)
|
|
17
|
+
|
|
18
|
+
ppm stop
|
|
19
|
+
Stop the PPM server (supervisor stays alive)
|
|
20
|
+
-a, --all — Kill all PPM and cloudflared processes (including untracked)
|
|
21
|
+
--kill — Full shutdown (kills supervisor too)
|
|
22
|
+
|
|
23
|
+
ppm down
|
|
24
|
+
Fully shut down PPM (supervisor + server + tunnel)
|
|
25
|
+
|
|
26
|
+
ppm restart
|
|
27
|
+
Restart the server (keeps tunnel alive)
|
|
28
|
+
-c, --config <path> — Path to config file
|
|
29
|
+
--force — Force resume from paused state
|
|
30
|
+
|
|
31
|
+
ppm status
|
|
32
|
+
Show PPM daemon status
|
|
33
|
+
-a, --all — Show all PPM and cloudflared processes (including untracked)
|
|
34
|
+
--json — Output as JSON
|
|
35
|
+
|
|
36
|
+
ppm open
|
|
37
|
+
Open PPM in browser
|
|
38
|
+
-c, --config <path> — Path to config file
|
|
39
|
+
|
|
40
|
+
ppm logs
|
|
41
|
+
View PPM daemon logs
|
|
42
|
+
-n, --tail <lines> — Number of lines to show [default: 50]
|
|
43
|
+
-f, --follow — Follow log output
|
|
44
|
+
--clear — Clear log file
|
|
45
|
+
|
|
46
|
+
ppm report
|
|
47
|
+
Report a bug on GitHub (pre-fills env info + logs)
|
|
48
|
+
|
|
49
|
+
ppm init
|
|
50
|
+
Initialize PPM configuration (interactive or via flags)
|
|
51
|
+
-p, --port <port> — Port to listen on
|
|
52
|
+
--scan <path> — Directory to scan for git repos
|
|
53
|
+
--auth — Enable authentication
|
|
54
|
+
--no-auth — Disable authentication
|
|
55
|
+
--password <pw> — Set access password
|
|
56
|
+
--share — Pre-install cloudflared for sharing
|
|
57
|
+
-y, --yes — Non-interactive mode (use defaults + flags)
|
|
58
|
+
|
|
59
|
+
ppm upgrade
|
|
60
|
+
Check for and install PPM updates
|
|
61
|
+
\`\`\`
|
|
62
|
+
## ppm projects — Manage registered projects
|
|
63
|
+
\`\`\`
|
|
64
|
+
ppm projects list
|
|
65
|
+
List all registered projects
|
|
66
|
+
|
|
67
|
+
ppm projects add <path>
|
|
68
|
+
Add a project to the registry
|
|
69
|
+
-n, --name <name> — Project name (defaults to folder name)
|
|
70
|
+
|
|
71
|
+
ppm projects remove <name>
|
|
72
|
+
Remove a project from the registry
|
|
73
|
+
\`\`\`
|
|
74
|
+
## ppm config — Configuration management
|
|
75
|
+
\`\`\`
|
|
76
|
+
ppm config get <key>
|
|
77
|
+
Get a config value (e.g. port, auth.enabled)
|
|
78
|
+
|
|
79
|
+
ppm config set <key> <value>
|
|
80
|
+
Set a config value (e.g. port 9090)
|
|
81
|
+
\`\`\`
|
|
82
|
+
## ppm git — Git operations for a project
|
|
83
|
+
\`\`\`
|
|
84
|
+
ppm git status
|
|
85
|
+
Show working tree status
|
|
86
|
+
-p, --project <name> — Project name or path
|
|
87
|
+
|
|
88
|
+
ppm git log
|
|
89
|
+
Show recent commits
|
|
90
|
+
-p, --project <name> — Project name or path
|
|
91
|
+
-n, --count <n> — Number of commits to show [default: 20]
|
|
92
|
+
|
|
93
|
+
ppm git diff [ref1] [ref2]
|
|
94
|
+
Show diff between refs or working tree
|
|
95
|
+
-p, --project <name> — Project name or path
|
|
96
|
+
|
|
97
|
+
ppm git stage <files...>
|
|
98
|
+
Stage files (use "." to stage all)
|
|
99
|
+
-p, --project <name> — Project name or path
|
|
100
|
+
|
|
101
|
+
ppm git unstage <files...>
|
|
102
|
+
Unstage files
|
|
103
|
+
-p, --project <name> — Project name or path
|
|
104
|
+
|
|
105
|
+
ppm git commit
|
|
106
|
+
Commit staged changes
|
|
107
|
+
-p, --project <name> — Project name or path
|
|
108
|
+
-m, --message <msg> — Commit message (required)
|
|
109
|
+
|
|
110
|
+
ppm git push
|
|
111
|
+
Push to remote
|
|
112
|
+
-p, --project <name> — Project name or path
|
|
113
|
+
--remote <remote> — Remote name [default: origin]
|
|
114
|
+
--branch <branch> — Branch name
|
|
115
|
+
|
|
116
|
+
ppm git pull
|
|
117
|
+
Pull from remote
|
|
118
|
+
-p, --project <name> — Project name or path
|
|
119
|
+
--remote <remote> — Remote name
|
|
120
|
+
--branch <branch> — Branch name
|
|
121
|
+
|
|
122
|
+
ppm git branch create <name>
|
|
123
|
+
Create and checkout a new branch
|
|
124
|
+
-p, --project <name> — Project name or path
|
|
125
|
+
--from <ref> — Base ref (commit/branch/tag)
|
|
126
|
+
|
|
127
|
+
ppm git branch checkout <name>
|
|
128
|
+
Switch to a branch
|
|
129
|
+
-p, --project <name> — Project name or path
|
|
130
|
+
|
|
131
|
+
ppm git branch delete <name>
|
|
132
|
+
Delete a branch
|
|
133
|
+
-p, --project <name> — Project name or path
|
|
134
|
+
-f, --force — Force delete
|
|
135
|
+
|
|
136
|
+
ppm git branch merge <source>
|
|
137
|
+
Merge a branch into current branch
|
|
138
|
+
-p, --project <name> — Project name or path
|
|
139
|
+
\`\`\`
|
|
140
|
+
## ppm chat — AI chat sessions
|
|
141
|
+
\`\`\`
|
|
142
|
+
ppm chat list
|
|
143
|
+
List all chat sessions
|
|
144
|
+
-p, --project <name> — Filter by project name
|
|
145
|
+
|
|
146
|
+
ppm chat create
|
|
147
|
+
Create a new chat session
|
|
148
|
+
-p, --project <name> — Project name or path
|
|
149
|
+
--provider <provider> — AI provider (default: claude)
|
|
150
|
+
|
|
151
|
+
ppm chat send <session-id> <message>
|
|
152
|
+
Send a message and stream response to stdout
|
|
153
|
+
-p, --project <name> — Project name or path
|
|
154
|
+
|
|
155
|
+
ppm chat resume <session-id>
|
|
156
|
+
Resume an interactive chat session
|
|
157
|
+
-p, --project <name> — Project name or path
|
|
158
|
+
|
|
159
|
+
ppm chat delete <session-id>
|
|
160
|
+
Delete a chat session
|
|
161
|
+
\`\`\`
|
|
162
|
+
## ppm db — Database connections & queries
|
|
163
|
+
\`\`\`
|
|
164
|
+
ppm db list
|
|
165
|
+
List all saved database connections
|
|
166
|
+
|
|
167
|
+
ppm db add
|
|
168
|
+
Add a new database connection
|
|
169
|
+
-n, --name <name> — Connection name (unique) (required)
|
|
170
|
+
-t, --type <type> — Database type: sqlite | postgres (required)
|
|
171
|
+
-c, --connection-string <url> — PostgreSQL connection string
|
|
172
|
+
-f, --file <path> — SQLite file path (absolute)
|
|
173
|
+
-g, --group <group> — Group name
|
|
174
|
+
--color <color> — Tab color (hex, e.g. #3b82f6)
|
|
175
|
+
|
|
176
|
+
ppm db remove <name>
|
|
177
|
+
Remove a saved connection (by name or ID)
|
|
178
|
+
|
|
179
|
+
ppm db test <name>
|
|
180
|
+
Test a saved connection
|
|
181
|
+
|
|
182
|
+
ppm db tables <name>
|
|
183
|
+
List tables in a database connection
|
|
184
|
+
|
|
185
|
+
ppm db schema <name> <table>
|
|
186
|
+
Show table schema (columns, types, constraints)
|
|
187
|
+
-s, --schema <schema> — PostgreSQL schema name [default: public]
|
|
188
|
+
|
|
189
|
+
ppm db data <name> <table>
|
|
190
|
+
View table data (paginated)
|
|
191
|
+
-p, --page <page> — Page number [default: 1]
|
|
192
|
+
-l, --limit <limit> — Rows per page [default: 50]
|
|
193
|
+
--order <column> — Order by column
|
|
194
|
+
--desc — Descending order
|
|
195
|
+
-s, --schema <schema> — PostgreSQL schema name [default: public]
|
|
196
|
+
|
|
197
|
+
ppm db query <name> <sql>
|
|
198
|
+
Execute a SQL query against a saved connection
|
|
199
|
+
\`\`\`
|
|
200
|
+
## ppm autostart — Auto-start on boot
|
|
201
|
+
\`\`\`
|
|
202
|
+
ppm autostart enable
|
|
203
|
+
Register PPM to start automatically on boot
|
|
204
|
+
-p, --port <port> — Override port
|
|
205
|
+
-s, --share — (deprecated) Tunnel is now always enabled
|
|
206
|
+
-c, --config <path> — Config file path
|
|
207
|
+
--profile <name> — DB profile name
|
|
208
|
+
|
|
209
|
+
ppm autostart disable
|
|
210
|
+
Remove PPM auto-start registration
|
|
211
|
+
|
|
212
|
+
ppm autostart status
|
|
213
|
+
Show auto-start status
|
|
214
|
+
--json — Output as JSON
|
|
215
|
+
\`\`\`
|
|
216
|
+
## ppm cloud — PPM Cloud — device registry + tunnel
|
|
217
|
+
\`\`\`
|
|
218
|
+
ppm cloud login
|
|
219
|
+
Sign in with Google
|
|
220
|
+
--url <url> — Cloud URL override
|
|
221
|
+
--device-code — Force device code flow (for remote terminals)
|
|
222
|
+
|
|
223
|
+
ppm cloud logout
|
|
224
|
+
Sign out from PPM Cloud
|
|
225
|
+
|
|
226
|
+
ppm cloud link
|
|
227
|
+
Register this machine with PPM Cloud
|
|
228
|
+
-n, --name <name> — Machine display name
|
|
229
|
+
|
|
230
|
+
ppm cloud unlink
|
|
231
|
+
Remove this machine from PPM Cloud
|
|
232
|
+
|
|
233
|
+
ppm cloud status
|
|
234
|
+
Show PPM Cloud connection status
|
|
235
|
+
--json — Output as JSON
|
|
236
|
+
|
|
237
|
+
ppm cloud devices
|
|
238
|
+
List all registered devices from cloud
|
|
239
|
+
--json — Output as JSON
|
|
240
|
+
\`\`\`
|
|
241
|
+
## ppm ext — Manage PPM extensions
|
|
242
|
+
\`\`\`
|
|
243
|
+
ppm ext install <name>
|
|
244
|
+
Install an extension from npm
|
|
245
|
+
|
|
246
|
+
ppm ext remove <name>
|
|
247
|
+
Remove an installed extension
|
|
248
|
+
|
|
249
|
+
ppm ext list
|
|
250
|
+
List installed extensions
|
|
251
|
+
|
|
252
|
+
ppm ext enable <name>
|
|
253
|
+
Enable an extension
|
|
254
|
+
|
|
255
|
+
ppm ext disable <name>
|
|
256
|
+
Disable an extension
|
|
257
|
+
|
|
258
|
+
ppm ext dev <path>
|
|
259
|
+
Symlink a local extension for development
|
|
260
|
+
\`\`\`
|
|
261
|
+
## ppm bot — PPMBot coordinator utilities
|
|
262
|
+
\`\`\`
|
|
263
|
+
ppm bot delegate
|
|
264
|
+
Delegate a task to a project subagent
|
|
265
|
+
--chat <id> — Telegram chat ID (required)
|
|
266
|
+
--project <name> — Project name (required)
|
|
267
|
+
--prompt <text> — Enriched task prompt (required)
|
|
268
|
+
--timeout <ms> — Timeout in milliseconds [default: 900000]
|
|
269
|
+
|
|
270
|
+
ppm bot task-status <id>
|
|
271
|
+
Get status of a delegated task
|
|
272
|
+
|
|
273
|
+
ppm bot task-result <id>
|
|
274
|
+
Get full result of a completed task
|
|
275
|
+
|
|
276
|
+
ppm bot tasks
|
|
277
|
+
List recent delegated tasks
|
|
278
|
+
--chat <id> — Telegram chat ID (auto-detected if single)
|
|
279
|
+
|
|
280
|
+
ppm bot memory save <content>
|
|
281
|
+
Save a cross-project memory
|
|
282
|
+
-c, --category <cat> — Category: fact|preference|decision|architecture|issue [default: fact]
|
|
283
|
+
-s, --session <id> — Session ID (optional)
|
|
284
|
+
|
|
285
|
+
ppm bot memory list
|
|
286
|
+
List active cross-project memories
|
|
287
|
+
-l, --limit <n> — Max results [default: 30]
|
|
288
|
+
--json — Output as JSON
|
|
289
|
+
|
|
290
|
+
ppm bot memory forget <topic>
|
|
291
|
+
Delete memories matching a topic (FTS5 search)
|
|
292
|
+
|
|
293
|
+
ppm bot project list
|
|
294
|
+
List available projects
|
|
295
|
+
--json — Output as JSON
|
|
296
|
+
|
|
297
|
+
ppm bot status
|
|
298
|
+
Show current status and running tasks
|
|
299
|
+
--chat <id> — Telegram chat ID (auto-detected if single)
|
|
300
|
+
--json — Output as JSON
|
|
301
|
+
|
|
302
|
+
ppm bot version
|
|
303
|
+
Show PPM version
|
|
304
|
+
|
|
305
|
+
ppm bot restart
|
|
306
|
+
Restart the PPM server
|
|
307
|
+
|
|
308
|
+
ppm bot help
|
|
309
|
+
Show all bot CLI commands
|
|
310
|
+
\`\`\`
|
|
311
|
+
|
|
312
|
+
## Quick Reference — Task Delegation
|
|
313
|
+
\`\`\`
|
|
314
|
+
ppm bot delegate --chat <chatId> --project <name> --prompt "<enriched task>"
|
|
315
|
+
ppm bot task-status <id>
|
|
316
|
+
ppm bot task-result <id>
|
|
317
|
+
ppm bot tasks
|
|
318
|
+
\`\`\`
|
|
319
|
+
|
|
320
|
+
## Quick Reference — Memory
|
|
321
|
+
\`\`\`
|
|
322
|
+
ppm bot memory save "<content>" -c <category>
|
|
323
|
+
ppm bot memory list
|
|
324
|
+
ppm bot memory forget "<topic>"
|
|
325
|
+
\`\`\`
|
|
326
|
+
|
|
327
|
+
## Tips
|
|
328
|
+
- Use \`--json\` flag when parsing command output programmatically
|
|
329
|
+
- For git/chat/db operations: always specify \`--project <name>\` or connection name
|
|
330
|
+
`;
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
markBotTaskReported,
|
|
15
15
|
} from "../db.service.ts";
|
|
16
16
|
import { PPMBotTelegram } from "./ppmbot-telegram.ts";
|
|
17
|
-
import { PPMBotSessionManager, ensureCoordinatorWorkspace, DEFAULT_COORDINATOR_IDENTITY } from "./ppmbot-session.ts";
|
|
17
|
+
import { PPMBotSessionManager, ensureCoordinatorWorkspace, readCliReference, DEFAULT_COORDINATOR_IDENTITY } from "./ppmbot-session.ts";
|
|
18
18
|
import { PPMBotMemory } from "./ppmbot-memory.ts";
|
|
19
19
|
import { streamToTelegram } from "./ppmbot-streamer.ts";
|
|
20
20
|
import { escapeHtml } from "./ppmbot-formatter.ts";
|
|
@@ -286,6 +286,13 @@ I'll answer directly or delegate to your project's AI.`;
|
|
|
286
286
|
parts.push(identity);
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
// CLI reference (from cli-reference.md)
|
|
290
|
+
const cliRef = readCliReference();
|
|
291
|
+
if (cliRef) {
|
|
292
|
+
parts.push(`\n## CLI Reference`);
|
|
293
|
+
parts.push(cliRef);
|
|
294
|
+
}
|
|
295
|
+
|
|
289
296
|
// Session info
|
|
290
297
|
parts.push(`\n## Session Info`);
|
|
291
298
|
parts.push(`Chat ID: ${chatId}`);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync as fsRead, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { chatService } from "../chat.service.ts";
|
|
5
5
|
import { configService } from "../config.service.ts";
|
|
6
|
+
import { VERSION } from "../../version.ts";
|
|
6
7
|
import {
|
|
7
8
|
getActivePPMBotSession,
|
|
8
9
|
createPPMBotSession,
|
|
@@ -10,57 +11,66 @@ import {
|
|
|
10
11
|
touchPPMBotSession,
|
|
11
12
|
getDistinctPPMBotProjectNames,
|
|
12
13
|
} from "../db.service.ts";
|
|
14
|
+
import { DEFAULT_CLI_REFERENCE } from "./cli-reference-default.ts";
|
|
13
15
|
import type { PPMBotActiveSession, PPMBotSessionRow } from "../../types/ppmbot.ts";
|
|
14
16
|
import type { PPMBotConfig, ProjectConfig } from "../../types/config.ts";
|
|
15
17
|
|
|
16
18
|
export const DEFAULT_COORDINATOR_IDENTITY = `# PPMBot — AI Project Coordinator
|
|
17
19
|
|
|
18
|
-
You are PPMBot, a personal AI project coordinator and team leader. You communicate with users via Telegram.
|
|
20
|
+
You are PPMBot, a personal AI project coordinator and team leader. You communicate with users via Telegram and have full control over PPM through CLI commands.
|
|
19
21
|
|
|
20
22
|
## Role
|
|
21
23
|
- Answer direct questions immediately (coding, general knowledge, quick advice)
|
|
22
24
|
- Delegate project-specific tasks to subagents using \`ppm bot delegate\`
|
|
23
25
|
- Track delegated task status and report results proactively
|
|
26
|
+
- Manage PPM server, projects, config, git, cloud, extensions, and databases
|
|
24
27
|
- Remember user preferences across conversations
|
|
25
|
-
- Act as a team leader coordinating work across multiple projects
|
|
26
28
|
|
|
27
29
|
## Decision Framework
|
|
28
30
|
1. Can I answer this directly without project context? → Answer now
|
|
29
|
-
2. Does this reference a specific project or need file access? → Delegate
|
|
30
|
-
3. Is this about PPM config
|
|
31
|
-
4.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
ppm
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
ppm
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
ppm
|
|
31
|
+
2. Does this reference a specific project or need file access? → Delegate with \`ppm bot delegate\`
|
|
32
|
+
3. Is this about PPM management (server/config/projects/git/db/cloud/ext)? → Use CLI commands directly
|
|
33
|
+
4. Is this a destructive operation? → Confirm with user first
|
|
34
|
+
5. Ambiguous project? → Ask user to clarify
|
|
35
|
+
|
|
36
|
+
## Safety Rules (CRITICAL)
|
|
37
|
+
Before executing destructive commands, ALWAYS confirm with the user:
|
|
38
|
+
- \`ppm stop\` / \`ppm down\` / \`ppm restart\` → "Are you sure you want to stop/restart PPM?"
|
|
39
|
+
- \`ppm db query\` with writes → warn about data modification risk
|
|
40
|
+
- \`ppm projects remove\` → confirm project name
|
|
41
|
+
- \`ppm config set\` → show current value with \`ppm config get\` BEFORE changing
|
|
42
|
+
- \`ppm cloud logout\` / \`ppm cloud unlink\` → confirm
|
|
43
|
+
- \`ppm git branch delete\` → warn about potential data loss
|
|
44
|
+
- \`ppm ext remove\` → confirm extension name
|
|
45
|
+
|
|
46
|
+
## Operational Patterns
|
|
47
|
+
- Before restart: check \`ppm status\` first
|
|
48
|
+
- Before config change: read current with \`ppm config get <key>\`
|
|
49
|
+
- Before git push: check \`ppm git status --project <name>\`
|
|
50
|
+
- For DB operations: always specify connection name
|
|
51
|
+
- For git operations: always use \`--project <name>\` flag
|
|
52
|
+
|
|
53
|
+
## CLI Commands
|
|
54
|
+
Full CLI reference is in \`cli-reference.md\` (auto-injected into context).
|
|
47
55
|
|
|
48
56
|
## Response Style
|
|
49
|
-
- Keep responses concise (Telegram
|
|
50
|
-
-
|
|
57
|
+
- Keep responses concise (Telegram — mobile-friendly)
|
|
58
|
+
- Short paragraphs, no walls of text
|
|
51
59
|
- When delegating: acknowledge immediately, notify on completion
|
|
52
60
|
- Support Vietnamese and English naturally
|
|
53
61
|
|
|
54
62
|
## Important
|
|
55
|
-
- When delegating, write
|
|
56
|
-
- Include
|
|
63
|
+
- When delegating, write enriched prompts with full context — not just raw user message
|
|
64
|
+
- Include: what user wants, which files/features, acceptance criteria
|
|
57
65
|
- Each delegation creates a fresh AI session in the target project workspace
|
|
66
|
+
- Use \`--json\` flag when parsing command output programmatically
|
|
58
67
|
`;
|
|
59
68
|
|
|
60
|
-
/** Ensure ~/.ppm/bot/ workspace exists with coordinator.md */
|
|
69
|
+
/** Ensure ~/.ppm/bot/ workspace exists with coordinator.md + cli-reference.md */
|
|
61
70
|
export function ensureCoordinatorWorkspace(): void {
|
|
62
71
|
const botDir = join(homedir(), ".ppm", "bot");
|
|
63
72
|
const coordinatorMd = join(botDir, "coordinator.md");
|
|
73
|
+
const cliRefPath = join(botDir, "cli-reference.md");
|
|
64
74
|
const settingsDir = join(botDir, ".claude");
|
|
65
75
|
const settingsFile = join(settingsDir, "settings.local.json");
|
|
66
76
|
|
|
@@ -75,6 +85,63 @@ export function ensureCoordinatorWorkspace(): void {
|
|
|
75
85
|
permissions: { allow: ["Bash", "Read", "Write", "Edit", "Glob", "Grep"] },
|
|
76
86
|
}, null, 2));
|
|
77
87
|
}
|
|
88
|
+
|
|
89
|
+
// Auto-generate cli-reference.md if missing or version mismatch
|
|
90
|
+
ensureCliReference(cliRefPath);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Read CLI reference from disk (for context injection) */
|
|
94
|
+
export function readCliReference(): string {
|
|
95
|
+
const cliRefPath = join(homedir(), ".ppm", "bot", "cli-reference.md");
|
|
96
|
+
try {
|
|
97
|
+
return fsRead(cliRefPath, "utf-8");
|
|
98
|
+
} catch {
|
|
99
|
+
return "";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Generate cli-reference.md if missing or version differs.
|
|
105
|
+
* Embeds version header: `<!-- ppm-version: x.x.x -->`
|
|
106
|
+
*/
|
|
107
|
+
function ensureCliReference(cliRefPath: string): void {
|
|
108
|
+
// Check existing version
|
|
109
|
+
if (existsSync(cliRefPath)) {
|
|
110
|
+
try {
|
|
111
|
+
const existing = fsRead(cliRefPath, "utf-8");
|
|
112
|
+
const versionMatch = existing.match(/<!-- ppm-version: (.+?) -->/);
|
|
113
|
+
if (versionMatch && versionMatch[1] === VERSION) return; // up to date
|
|
114
|
+
} catch { /* regenerate on read error */ }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const content = generateCliReference();
|
|
119
|
+
writeFileSync(cliRefPath, content);
|
|
120
|
+
console.log(`[ppmbot] Generated cli-reference.md (v${VERSION})`);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
console.warn(`[ppmbot] Failed to generate cli-reference.md:`, (err as Error).message);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Generate CLI reference by running the generator script */
|
|
127
|
+
function generateCliReference(): string {
|
|
128
|
+
const { spawnSync } = require("node:child_process") as typeof import("node:child_process");
|
|
129
|
+
const scriptPath = join(import.meta.dir, "../../../scripts/generate-bot-coordinator.ts");
|
|
130
|
+
|
|
131
|
+
// If generator script exists (dev/source install), run it
|
|
132
|
+
if (existsSync(scriptPath)) {
|
|
133
|
+
const result = spawnSync("bun", [scriptPath, "--cli-only"], {
|
|
134
|
+
cwd: join(import.meta.dir, "../../.."),
|
|
135
|
+
timeout: 10_000,
|
|
136
|
+
encoding: "utf-8",
|
|
137
|
+
});
|
|
138
|
+
if (result.status === 0 && result.stdout) {
|
|
139
|
+
return `<!-- ppm-version: ${VERSION} -->\n${result.stdout}`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Fallback: generate from bundled constant
|
|
144
|
+
return `<!-- ppm-version: ${VERSION} -->\n${DEFAULT_CLI_REFERENCE}`;
|
|
78
145
|
}
|
|
79
146
|
|
|
80
147
|
export class PPMBotSessionManager {
|
|
@@ -65,6 +65,8 @@ let tunnelProbeTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
65
65
|
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
66
66
|
let upgradeCheckTimer: ReturnType<typeof setInterval> | null = null;
|
|
67
67
|
let upgradeDelayTimer: ReturnType<typeof setTimeout> | null = null;
|
|
68
|
+
let cloudMonitorTimer: ReturnType<typeof setInterval> | null = null;
|
|
69
|
+
let cloudConnected = false; // tracks whether we've initiated a cloud WS connection
|
|
68
70
|
|
|
69
71
|
// Saved at startup for self-replace
|
|
70
72
|
let originalArgv: string[] = [];
|
|
@@ -488,11 +490,11 @@ async function notifyStateChange(from: string, to: string, reason: string) {
|
|
|
488
490
|
}
|
|
489
491
|
|
|
490
492
|
/** Connect supervisor to Cloud via WebSocket (if device is linked) */
|
|
491
|
-
async function connectCloud(opts: { port: number }, serverArgs: string[], logFd: number) {
|
|
493
|
+
async function connectCloud(opts: { port: number }, serverArgs: string[], logFd: number): Promise<boolean> {
|
|
492
494
|
try {
|
|
493
495
|
const { getCloudDevice } = await import("./cloud.service.ts");
|
|
494
496
|
const device = getCloudDevice();
|
|
495
|
-
if (!device) return; // not linked to cloud
|
|
497
|
+
if (!device) return false; // not linked to cloud
|
|
496
498
|
|
|
497
499
|
const { connect, onCommand } = await import("./cloud-ws.service.ts");
|
|
498
500
|
const { VERSION } = await import("../version.ts");
|
|
@@ -609,11 +611,48 @@ async function connectCloud(opts: { port: number }, serverArgs: string[], logFd:
|
|
|
609
611
|
sendResult(false, `Unknown action: ${cmd.action}`);
|
|
610
612
|
}
|
|
611
613
|
});
|
|
614
|
+
cloudConnected = true;
|
|
615
|
+
return true;
|
|
612
616
|
} catch (e) {
|
|
613
617
|
log("WARN", `Cloud WS setup failed: ${e}`);
|
|
618
|
+
return false;
|
|
614
619
|
}
|
|
615
620
|
}
|
|
616
621
|
|
|
622
|
+
/** Periodically check if cloud-device.json appeared/disappeared and connect/disconnect */
|
|
623
|
+
function startCloudMonitor(opts: { port: number }, serverArgs: string[], logFd: number) {
|
|
624
|
+
const CLOUD_MONITOR_INTERVAL_MS = 60_000; // check every 60s
|
|
625
|
+
cloudMonitorTimer = setInterval(async () => {
|
|
626
|
+
if (shuttingDown) return;
|
|
627
|
+
try {
|
|
628
|
+
const { getCloudDevice } = await import("./cloud.service.ts");
|
|
629
|
+
const device = getCloudDevice();
|
|
630
|
+
const { isConnected } = await import("./cloud-ws.service.ts");
|
|
631
|
+
|
|
632
|
+
if (device && !cloudConnected) {
|
|
633
|
+
// Device linked but WS not connected — connect now
|
|
634
|
+
log("INFO", "Cloud monitor: device linked detected, connecting to cloud");
|
|
635
|
+
await connectCloud(opts, serverArgs, logFd);
|
|
636
|
+
} else if (device && cloudConnected && !isConnected()) {
|
|
637
|
+
// Device linked, we attempted connection but WS is dead — reconnect
|
|
638
|
+
log("WARN", "Cloud monitor: WS disconnected, reconnecting");
|
|
639
|
+
const { disconnect } = await import("./cloud-ws.service.ts");
|
|
640
|
+
disconnect();
|
|
641
|
+
cloudConnected = false;
|
|
642
|
+
await connectCloud(opts, serverArgs, logFd);
|
|
643
|
+
} else if (!device && cloudConnected) {
|
|
644
|
+
// Device unlinked — disconnect
|
|
645
|
+
log("INFO", "Cloud monitor: device unlinked, disconnecting from cloud");
|
|
646
|
+
const { disconnect } = await import("./cloud-ws.service.ts");
|
|
647
|
+
disconnect();
|
|
648
|
+
cloudConnected = false;
|
|
649
|
+
}
|
|
650
|
+
} catch (e) {
|
|
651
|
+
log("WARN", `Cloud monitor error: ${e}`);
|
|
652
|
+
}
|
|
653
|
+
}, CLOUD_MONITOR_INTERVAL_MS);
|
|
654
|
+
}
|
|
655
|
+
|
|
617
656
|
// ─── Soft stop (server only, supervisor stays alive) ──────────────────
|
|
618
657
|
let _softStopRunning = false;
|
|
619
658
|
export async function softStop() {
|
|
@@ -674,6 +713,7 @@ export function shutdown() {
|
|
|
674
713
|
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
675
714
|
if (upgradeCheckTimer) clearInterval(upgradeCheckTimer);
|
|
676
715
|
if (upgradeDelayTimer) clearTimeout(upgradeDelayTimer);
|
|
716
|
+
if (cloudMonitorTimer) clearInterval(cloudMonitorTimer);
|
|
677
717
|
|
|
678
718
|
if (serverChild) { try { serverChild.kill(); } catch {} }
|
|
679
719
|
if (tunnelChild) { try { tunnelChild.kill(); } catch {} }
|
|
@@ -801,8 +841,9 @@ export async function runSupervisor(opts: {
|
|
|
801
841
|
}, 1000);
|
|
802
842
|
}
|
|
803
843
|
|
|
804
|
-
// Connect to Cloud via WebSocket (if device is linked)
|
|
844
|
+
// Connect to Cloud via WebSocket (if device is linked) + start monitoring
|
|
805
845
|
connectCloud(opts, serverArgs, logFd);
|
|
846
|
+
startCloudMonitor(opts, serverArgs, logFd);
|
|
806
847
|
|
|
807
848
|
// Spawn server + tunnel in parallel
|
|
808
849
|
const promises: Promise<void>[] = [spawnServer(serverArgs, logFd)];
|