@hienlh/ppm 0.9.34 → 0.9.35

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 CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.35] - 2026-04-06
4
+
5
+ ### Fixed
6
+ - **Stream hangs forever on stuck AI**: Replaced elapsed-time timeout with per-event `Promise.race` (60s per event). If `.next()` hangs, stream is terminated cleanly instead of blocking forever.
7
+ - **"Project not found" when no default configured**: Added `~/.ppm/bot/` fallback project so bot works immediately without project setup.
8
+
9
+ ### Added
10
+ - **Identity onboarding**: `/start` now prompts new users for name, role, stack, and language preference when no identity memories exist.
11
+
3
12
  ## [0.9.34] - 2026-04-06
4
13
 
5
14
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.9.34",
3
+ "version": "0.9.35",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -184,10 +184,25 @@ class PPMBotService {
184
184
  }
185
185
  text += "\nSwitch: /project <name>";
186
186
  } else {
187
- text += "⚠️ No projects yet. Add one in PPM Settings first.";
187
+ text += "No projects configured I'll use a default workspace.";
188
188
  }
189
189
  text += "\n\nJust send a message to start chatting, or /help for commands.";
190
190
  await this.telegram!.sendMessage(Number(chatId), text);
191
+
192
+ // Identity onboarding: if no identity memories exist, ask user
193
+ const identityMemories = this.memory.recall("_global", "user identity name role");
194
+ if (identityMemories.length === 0) {
195
+ await this.telegram!.sendMessage(
196
+ Number(chatId),
197
+ "📝 <b>Quick intro?</b>\n\n" +
198
+ "I don't know much about you yet! Tell me:\n" +
199
+ "• Your name\n" +
200
+ "• What you work on (language, stack, role)\n" +
201
+ "• Preferred response language (English, Vietnamese, etc.)\n\n" +
202
+ "I'll remember your preferences for future chats.\n" +
203
+ "Or skip this and just start chatting!",
204
+ );
205
+ }
191
206
  }
192
207
 
193
208
  private async cmdProject(chatId: string, args: string): Promise<void> {
@@ -1,3 +1,6 @@
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
1
4
  import { chatService } from "../chat.service.ts";
2
5
  import { configService } from "../config.service.ts";
3
6
  import {
@@ -29,9 +32,10 @@ export class PPMBotSessionManager {
29
32
  return cached;
30
33
  }
31
34
 
32
- const resolvedProject = this.resolveProject(
33
- projectName || this.getDefaultProject(),
34
- );
35
+ const input = projectName || this.getDefaultProject();
36
+ const resolvedProject = input
37
+ ? this.resolveProject(input)
38
+ : this.getFallbackProject();
35
39
  if (!resolvedProject) {
36
40
  throw new Error(`Project not found: "${projectName || "(default)"}"`);
37
41
  }
@@ -128,6 +132,13 @@ export class PPMBotSessionManager {
128
132
  return cfg?.default_project || "";
129
133
  }
130
134
 
135
+ /** Fallback project when nothing is configured: ~/.ppm/bot/ */
136
+ private getFallbackProject(): { name: string; path: string } {
137
+ const botDir = join(homedir(), ".ppm", "bot");
138
+ if (!existsSync(botDir)) mkdirSync(botDir, { recursive: true });
139
+ return { name: "bot", path: botDir };
140
+ }
141
+
131
142
  private getDefaultProvider(): string {
132
143
  const cfg = configService.get("clawbot") as PPMBotConfig | undefined;
133
144
  return cfg?.default_provider || configService.get("ai").default_provider;
@@ -8,9 +8,37 @@ import {
8
8
 
9
9
  const MAX_MSG_LEN = 4096;
10
10
  const TYPING_REFRESH_MS = 4000;
11
- const STREAM_TIMEOUT_MS = 5 * 60 * 1000; // 5 min max per stream
11
+ const EVENT_TIMEOUT_MS = 60_000; // 60s max wait per event
12
12
  const PLACEHOLDER = "\u2026"; // ellipsis
13
13
 
14
+ /**
15
+ * Wrap an async iterable with per-event timeout.
16
+ * If .next() doesn't resolve within timeoutMs, yields a timeout error.
17
+ */
18
+ async function* withEventTimeout<T>(
19
+ iterable: AsyncIterable<T>,
20
+ timeoutMs: number,
21
+ ): AsyncGenerator<T> {
22
+ const iterator = iterable[Symbol.asyncIterator]();
23
+ try {
24
+ while (true) {
25
+ const result = await Promise.race([
26
+ iterator.next(),
27
+ new Promise<{ done: true; value: undefined; timedOut: true }>((resolve) =>
28
+ setTimeout(() => resolve({ done: true, value: undefined, timedOut: true }), timeoutMs),
29
+ ),
30
+ ]);
31
+ if ("timedOut" in result) {
32
+ throw new Error("No response within 60 seconds");
33
+ }
34
+ if (result.done) break;
35
+ yield result.value;
36
+ }
37
+ } finally {
38
+ iterator.return?.();
39
+ }
40
+ }
41
+
14
42
  export interface StreamConfig {
15
43
  showToolCalls: boolean;
16
44
  showThinking: boolean;
@@ -120,14 +148,9 @@ export async function streamToTelegram(
120
148
  await telegram.editMessage(chatId, currentMsgId, html);
121
149
  };
122
150
 
123
- // Process event stream with timeout
124
- const streamStart = Date.now();
151
+ // Process event stream with per-event timeout
125
152
  try {
126
- for await (const event of events) {
127
- if (Date.now() - streamStart > STREAM_TIMEOUT_MS) {
128
- appendHtml(segments, "\n\n⏱️ <i>Response timed out.</i>");
129
- break;
130
- }
153
+ for await (const event of withEventTimeout(events, EVENT_TIMEOUT_MS)) {
131
154
  await refreshTyping();
132
155
 
133
156
  switch (event.type) {