@mestreyoda/fabrica 0.1.8 → 0.1.10

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/dist/index.js CHANGED
@@ -111329,8 +111329,8 @@ import fsSync from "node:fs";
111329
111329
  import path5 from "node:path";
111330
111330
  import { fileURLToPath as fileURLToPath3 } from "node:url";
111331
111331
  function getCurrentVersion() {
111332
- if ("0.1.8") {
111333
- return "0.1.8";
111332
+ if ("0.1.10") {
111333
+ return "0.1.10";
111334
111334
  }
111335
111335
  try {
111336
111336
  const pkgPath = path5.join(THIS_DIR, "..", "..", "package.json");
@@ -142032,6 +142032,9 @@ function registerAttachmentHook(api, ctx) {
142032
142032
 
142033
142033
  // lib/dispatch/telegram-bootstrap-hook.ts
142034
142034
  import { homedir as homedir3 } from "node:os";
142035
+ init_runtime_paths();
142036
+ init_extract_json();
142037
+ init_zod();
142035
142038
 
142036
142039
  // lib/dispatch/telegram-bootstrap-session.ts
142037
142040
  init_migrate_layout();
@@ -142039,6 +142042,7 @@ import { createHash as createHash6 } from "node:crypto";
142039
142042
  import fs38 from "node:fs/promises";
142040
142043
  import path40 from "node:path";
142041
142044
  var SESSION_TTL_MS = 10 * 6e4;
142045
+ var CLASSIFYING_TTL_MS = 15e3;
142042
142046
  function sessionsDir(workspaceDir) {
142043
142047
  return path40.join(workspaceDir, DATA_DIR, "bootstrap-sessions");
142044
142048
  }
@@ -142063,14 +142067,15 @@ function buildBootstrapRequestFingerprint(input) {
142063
142067
  function buildBootstrapRequestHash(input) {
142064
142068
  return buildBootstrapRequestFingerprint(input);
142065
142069
  }
142066
- function nextSuppressUntil() {
142067
- return new Date(Date.now() + SESSION_TTL_MS).toISOString();
142070
+ function nextSuppressUntil(status) {
142071
+ const ttl = status === "classifying" ? CLASSIFYING_TTL_MS : SESSION_TTL_MS;
142072
+ return new Date(Date.now() + ttl).toISOString();
142068
142073
  }
142069
142074
  async function readTelegramBootstrapSession(workspaceDir, conversationId) {
142070
142075
  try {
142071
142076
  const raw = await fs38.readFile(sessionPath(workspaceDir, conversationId), "utf-8");
142072
142077
  const session = JSON.parse(raw);
142073
- if (session.status === "clarifying" && Date.parse(session.suppressUntil) < Date.now()) {
142078
+ if ((session.status === "clarifying" || session.status === "classifying") && Date.parse(session.suppressUntil) < Date.now()) {
142074
142079
  await fs38.unlink(sessionPath(workspaceDir, conversationId)).catch(() => {
142075
142080
  });
142076
142081
  return null;
@@ -142081,6 +142086,10 @@ async function readTelegramBootstrapSession(workspaceDir, conversationId) {
142081
142086
  throw error48;
142082
142087
  }
142083
142088
  }
142089
+ async function deleteTelegramBootstrapSession(workspaceDir, conversationId) {
142090
+ await fs38.unlink(sessionPath(workspaceDir, conversationId)).catch(() => {
142091
+ });
142092
+ }
142084
142093
  async function writeTelegramBootstrapSession(workspaceDir, session) {
142085
142094
  const dir = sessionsDir(workspaceDir);
142086
142095
  await fs38.mkdir(dir, { recursive: true });
@@ -142117,12 +142126,13 @@ async function upsertTelegramBootstrapSession(workspaceDir, input) {
142117
142126
  issueId: input.issueId ?? existing?.issueId ?? null,
142118
142127
  messageThreadId: input.messageThreadId ?? existing?.messageThreadId ?? null,
142119
142128
  projectChannelId: input.projectChannelId ?? existing?.projectChannelId ?? null,
142129
+ language: input.language ?? existing?.language,
142120
142130
  status: input.status,
142121
142131
  pendingClarification: input.pendingClarification !== void 0 ? input.pendingClarification : existing?.pendingClarification ?? null,
142122
142132
  orphanedArtifacts: input.orphanedArtifacts !== void 0 ? input.orphanedArtifacts : existing?.orphanedArtifacts ?? null,
142123
142133
  createdAt: existing?.createdAt ?? now2,
142124
142134
  updatedAt: now2,
142125
- suppressUntil: nextSuppressUntil(),
142135
+ suppressUntil: nextSuppressUntil(input.status),
142126
142136
  error: input.error ?? null
142127
142137
  };
142128
142138
  await writeTelegramBootstrapSession(workspaceDir, session);
@@ -142137,6 +142147,30 @@ function shouldSuppressTelegramBootstrapReply(session, request2) {
142137
142147
  }
142138
142148
 
142139
142149
  // lib/dispatch/telegram-bootstrap-hook.ts
142150
+ var BOOTSTRAP_MESSAGES = {
142151
+ ack: {
142152
+ pt: "Recebi! Vou analisar e come\xE7ar a montar o projeto...",
142153
+ en: "Got it! I'll analyze your request and start setting up the project..."
142154
+ },
142155
+ clarifyStack: {
142156
+ pt: "Qual stack voc\xEA quer usar? (Python, Node.js, Go, Java...)",
142157
+ en: "Which stack do you want to use? (Python, Node.js, Go, Java...)"
142158
+ },
142159
+ clarifyBoth: {
142160
+ pt: "Beleza! S\xF3 preciso de duas coisas pra criar:\n\n1. Qual stack? (Python, Node.js, Go, Java...)\n2. Quer dar um nome pro projeto? Se n\xE3o, eu invento um.",
142161
+ en: "Great! I just need two things:\n\n1. Which stack? (Python, Node.js, Go, Java...)\n2. Want to name the project? If not, I'll pick one."
142162
+ },
142163
+ clarifyStackFollowUp: {
142164
+ pt: "N\xE3o consegui identificar a stack. Pode me dizer qual linguagem/framework voc\xEA quer usar? Ex: Python, Node.js, Go, Java...",
142165
+ en: "Couldn't identify the stack. Can you tell me which language/framework you'd like to use? e.g., Python, Node.js, Go, Java..."
142166
+ },
142167
+ registered: {
142168
+ pt: (name, link) => `Projeto "${name}" registrado.
142169
+ Vou continuar o fluxo em ${link}`,
142170
+ en: (name, link) => `Project "${name}" registered.
142171
+ I'll continue the flow at ${link}`
142172
+ }
142173
+ };
142140
142174
  function inferProjectSlug(text) {
142141
142175
  const slug = text.toLowerCase().normalize("NFKD").replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 64);
142142
142176
  return slug || void 0;
@@ -142184,6 +142218,57 @@ function parseBootstrapRequest(text) {
142184
142218
  stackHint
142185
142219
  };
142186
142220
  }
142221
+ var MAX_CLASSIFY_LENGTH = 500;
142222
+ function isAmbiguousCandidate(text) {
142223
+ const lower2 = text.toLowerCase();
142224
+ if (lower2.length <= 20 || lower2.length > MAX_CLASSIFY_LENGTH) return false;
142225
+ const softwareCue = /\b(projeto|project|cli|api|app|aplicativo|servi[cç]o|library|biblioteca|repo|reposit[oó]rio|tool|ferramenta|sistema|system|bot|script|programa|program)\b/.test(lower2);
142226
+ return softwareCue;
142227
+ }
142228
+ var DmIntentSchema = external_exports.object({
142229
+ intent: external_exports.enum(["create_project", "other"]),
142230
+ confidence: external_exports.number().min(0).max(1),
142231
+ stackHint: external_exports.string().nullable().optional(),
142232
+ projectSlug: external_exports.string().nullable().optional(),
142233
+ language: external_exports.enum(["pt", "en"]).optional().default("pt")
142234
+ });
142235
+ var CLASSIFY_PROMPT_TEMPLATE = `Classify this Telegram DM. Is the user asking to create/build a new software project, or is it something else (question, greeting, status check)?
142236
+
142237
+ Message: "$CONTENT"
142238
+
142239
+ Return ONLY valid JSON:
142240
+ {"intent": "create_project" | "other", "confidence": 0.0-1.0, "stackHint": "<detected stack or null>", "projectSlug": "<suggested slug or null>", "language": "pt" | "en"}
142241
+
142242
+ Examples:
142243
+ - "Cria uma CLI Python que valida CPF" \u2192 {"intent":"create_project","confidence":0.95,"stackHint":"python-cli","projectSlug":"validador-cpf-cli","language":"pt"}
142244
+ - "Build me a REST API for tasks" \u2192 {"intent":"create_project","confidence":0.9,"stackHint":"fastapi","projectSlug":"task-api","language":"en"}
142245
+ - "How's the project going?" \u2192 {"intent":"other","confidence":0.95,"stackHint":null,"projectSlug":null,"language":"en"}
142246
+ - "Oi, tudo bem?" \u2192 {"intent":"other","confidence":0.99,"stackHint":null,"projectSlug":null,"language":"pt"}
142247
+ - "Me faz um app que converte temperaturas" \u2192 {"intent":"create_project","confidence":0.9,"stackHint":null,"projectSlug":"conversor-temperaturas","language":"pt"}`;
142248
+ async function classifyDmIntent(ctx, content, workspaceDir) {
142249
+ try {
142250
+ const truncated = content.slice(0, MAX_CLASSIFY_LENGTH);
142251
+ const prompt = CLASSIFY_PROMPT_TEMPLATE.replace("$CONTENT", truncated.replace(/"/g, '\\"'));
142252
+ const cliPath = resolveOpenClawCli({ homeDir: homedir3(), workspaceDir });
142253
+ const sessionId = `dm-classify-${Date.now()}`;
142254
+ const result = await ctx.runCommand(
142255
+ [cliPath, "agent", "--local", "-m", prompt, "--session-id", sessionId, "--json"],
142256
+ { timeoutMs: 15e3 }
142257
+ );
142258
+ const stdout = result.stdout ?? "";
142259
+ if (!stdout.trim()) return null;
142260
+ const parsed = extractJsonFromStdout(stdout);
142261
+ if (!parsed) return null;
142262
+ const text = parsed?.payloads?.[0]?.text;
142263
+ const jsonStr = text ? text.replace(/^```(json)?/gm, "").replace(/```$/gm, "").trim() : JSON.stringify(parsed);
142264
+ const intentData = JSON.parse(jsonStr);
142265
+ const validated = DmIntentSchema.safeParse(intentData);
142266
+ if (!validated.success) return null;
142267
+ return validated.data;
142268
+ } catch {
142269
+ return null;
142270
+ }
142271
+ }
142187
142272
  function isBootstrapCandidate(text) {
142188
142273
  const lower2 = text.toLowerCase();
142189
142274
  if (/^\s*(project name|nome do projeto|repository url|repo url|stack)\s*:/im.test(text)) return true;
@@ -142226,26 +142311,23 @@ function parseClarificationResponse(text, session) {
142226
142311
  }
142227
142312
  return { recognized: false };
142228
142313
  }
142229
- function buildClarificationMessage(parsed, pendingClarification) {
142314
+ function buildClarificationMessage(parsed, pendingClarification, language = "pt") {
142230
142315
  if (pendingClarification === "stack_and_name" || !parsed.stackHint && !parsed.projectName) {
142231
- return `Beleza! S\xF3 preciso de duas coisas pra criar:
142232
-
142233
- 1. Qual stack? (Python, Node.js, Go, Java...)
142234
- 2. Quer dar um nome pro projeto? Se n\xE3o, eu invento um.`;
142316
+ return BOOTSTRAP_MESSAGES.clarifyBoth[language];
142235
142317
  }
142236
- return `Qual stack voc\xEA quer usar? (Python, Node.js, Go, Java...)`;
142318
+ return BOOTSTRAP_MESSAGES.clarifyStack[language];
142237
142319
  }
142238
142320
  function buildFollowUpClarification(session) {
142239
- if (!session.stackHint) {
142240
- return `N\xE3o consegui identificar a stack. Pode me dizer qual linguagem/framework voc\xEA quer usar? Ex: Python, Node.js, Go, Java...`;
142241
- }
142242
- return `Pode me dar mais detalhes sobre o que voc\xEA quer construir?`;
142321
+ const lang = session.language ?? "pt";
142322
+ if (!session.stackHint) return BOOTSTRAP_MESSAGES.clarifyStackFollowUp[lang];
142323
+ return lang === "en" ? "Can you give me more details about what you want to build?" : "Pode me dar mais detalhes sobre o que voc\xEA quer construir?";
142243
142324
  }
142244
- function buildDmAck(projectName, topicName) {
142245
- return [
142246
- `Projeto "${projectName}" registrado.`,
142247
- `Vou continuar o fluxo no t\xF3pico "${topicName}" do grupo de projetos.`
142248
- ].join("\n");
142325
+ function buildTopicDeepLink(chatId, topicId) {
142326
+ const stripped = chatId.replace(/^-100/, "");
142327
+ return `https://t.me/c/${stripped}/${topicId}`;
142328
+ }
142329
+ function buildDmAck(projectName, topicLink, language = "pt") {
142330
+ return BOOTSTRAP_MESSAGES.registered[language](projectName, topicLink);
142249
142331
  }
142250
142332
  function buildTopicKickoff(projectName, idea) {
142251
142333
  return [
@@ -142274,6 +142356,70 @@ function logBootstrapWarning(ctx, message) {
142274
142356
  ctx.logger.info(message);
142275
142357
  }
142276
142358
  }
142359
+ async function classifyAndBootstrap(ctx, workspaceDir, conversationId, content) {
142360
+ const classification = await classifyDmIntent(ctx, content, workspaceDir);
142361
+ if (!classification || classification.intent !== "create_project" || classification.confidence < 0.7) {
142362
+ if (!classification) {
142363
+ logBootstrapWarning(ctx, `[telegram-bootstrap] LLM classify failed, falling back (conversation: ${conversationId})`);
142364
+ }
142365
+ await deleteTelegramBootstrapSession(workspaceDir, conversationId);
142366
+ return;
142367
+ }
142368
+ const language = classification.language ?? "pt";
142369
+ await sendTelegramText(ctx, conversationId, BOOTSTRAP_MESSAGES.ack[language]);
142370
+ const parsed = parseBootstrapRequest(content);
142371
+ if (classification.stackHint && !parsed.stackHint) {
142372
+ parsed.stackHint = classification.stackHint;
142373
+ }
142374
+ if (classification.projectSlug && !parsed.projectName) {
142375
+ parsed.projectName = classification.projectSlug;
142376
+ }
142377
+ const incomingRequest = {
142378
+ rawIdea: parsed.rawIdea,
142379
+ projectName: parsed.projectName ?? null,
142380
+ stackHint: parsed.stackHint ?? null,
142381
+ repoUrl: parsed.repoUrl ?? null,
142382
+ repoPath: parsed.repoPath ?? null
142383
+ };
142384
+ const incomingRequestHash = buildBootstrapRequestHash(incomingRequest);
142385
+ const sessionForHash = await readTelegramBootstrapSession(workspaceDir, conversationId);
142386
+ if (sessionForHash?.requestHash === incomingRequestHash) {
142387
+ if (sessionForHash.status === "completed") {
142388
+ ctx.logger.info(`[telegram-bootstrap] duplicate completed DM ignored (LLM path) for conversation ${conversationId}`);
142389
+ return;
142390
+ }
142391
+ const isExpiredReceived = sessionForHash.status === "received" && Date.parse(sessionForHash.suppressUntil) < Date.now();
142392
+ if (sessionForHash.status !== "failed" && sessionForHash.status !== "classifying" && !isExpiredReceived) {
142393
+ ctx.logger.info(`[telegram-bootstrap] duplicate in-flight DM ignored (LLM path) for conversation ${conversationId}`);
142394
+ return;
142395
+ }
142396
+ }
142397
+ const sourceRoute = { channel: "telegram", channelId: conversationId };
142398
+ const session = await upsertTelegramBootstrapSession(workspaceDir, {
142399
+ conversationId,
142400
+ ...incomingRequest,
142401
+ sourceRoute,
142402
+ sourceChannel: "telegram",
142403
+ status: "received",
142404
+ language
142405
+ });
142406
+ if (!parsed.stackHint) {
142407
+ const pendingClarification = !parsed.projectName ? "stack_and_name" : "stack";
142408
+ await upsertTelegramBootstrapSession(workspaceDir, {
142409
+ conversationId,
142410
+ ...incomingRequest,
142411
+ sourceRoute: session.sourceRoute,
142412
+ status: "clarifying",
142413
+ pendingClarification,
142414
+ language
142415
+ });
142416
+ await sendTelegramText(ctx, conversationId, buildClarificationMessage(parsed, pendingClarification, language));
142417
+ return;
142418
+ }
142419
+ continueBootstrap(ctx, conversationId, workspaceDir, incomingRequest, sourceRoute).catch((err) => {
142420
+ logBootstrapWarning(ctx, `[telegram-bootstrap] unhandled pipeline error (LLM path): ${err instanceof Error ? err.message : String(err)}`);
142421
+ });
142422
+ }
142277
142423
  async function continueBootstrap(ctx, conversationId, workspaceDir, request2, sourceRoute) {
142278
142424
  const telegramConfig = readFabricaTelegramConfig(ctx.pluginConfig);
142279
142425
  if (!telegramConfig.projectsForumChatId) {
@@ -142429,7 +142575,8 @@ Erro: ${result.error ?? "erro desconhecido"}`
142429
142575
  logBootstrapWarning(ctx, `[telegram-bootstrap] immediate projectTick failed: ${error48 instanceof Error ? error48.message : String(error48)}`);
142430
142576
  });
142431
142577
  }
142432
- await sendTelegramText(ctx, conversationId, buildDmAck(resolvedProjectName, `${projectChannelId}:${messageThreadId}`));
142578
+ const sessionLang = currentSession?.language ?? "pt";
142579
+ await sendTelegramText(ctx, conversationId, buildDmAck(resolvedProjectName, buildTopicDeepLink(String(projectChannelId), messageThreadId), sessionLang));
142433
142580
  await upsertTelegramBootstrapSession(workspaceDir, {
142434
142581
  conversationId,
142435
142582
  ...incomingRequest,
@@ -142478,6 +142625,18 @@ function registerTelegramBootstrapHook(api, ctx) {
142478
142625
  ].join("\n")
142479
142626
  };
142480
142627
  });
142628
+ api.on("message_sending", async (_event, eventCtx) => {
142629
+ const hookCtx = eventCtx;
142630
+ if (hookCtx.channelId !== "telegram") return;
142631
+ const conversationId = String(hookCtx.conversationId ?? "").trim();
142632
+ if (!conversationId || conversationId.includes(":topic:") || conversationId.startsWith("-")) return;
142633
+ const workspaceDir = resolveWorkspaceDir(ctx.config);
142634
+ if (!workspaceDir) return;
142635
+ const session = await readTelegramBootstrapSession(workspaceDir, conversationId);
142636
+ if (session && session.status !== "completed" && session.status !== "failed" && shouldSuppressTelegramBootstrapReply(session)) {
142637
+ return { cancel: true };
142638
+ }
142639
+ });
142481
142640
  api.on("message_received", async (event, eventCtx) => {
142482
142641
  if (eventCtx.channelId !== "telegram") return;
142483
142642
  const telegramConfig = readFabricaTelegramConfig(ctx.pluginConfig);
@@ -142495,6 +142654,10 @@ function registerTelegramBootstrapHook(api, ctx) {
142495
142654
  }
142496
142655
  const existingSession = await readTelegramBootstrapSession(workspaceDir, conversationId);
142497
142656
  const sessionIsExpired = existingSession != null && Date.parse(existingSession.suppressUntil) < Date.now();
142657
+ if (existingSession && !sessionIsExpired && existingSession.status === "classifying") {
142658
+ ctx.logger.info(`[telegram-bootstrap] LLM classification in progress for ${conversationId}, ignoring concurrent message`);
142659
+ return;
142660
+ }
142498
142661
  if (existingSession?.status === "clarifying" && !sessionIsExpired) {
142499
142662
  const clarResult = parseClarificationResponse(content, existingSession);
142500
142663
  if (!clarResult.recognized) {
@@ -142518,7 +142681,20 @@ function registerTelegramBootstrapHook(api, ctx) {
142518
142681
  });
142519
142682
  return;
142520
142683
  }
142521
- if (!isBootstrapCandidate(content)) return;
142684
+ if (!isBootstrapCandidate(content)) {
142685
+ if (isAmbiguousCandidate(content)) {
142686
+ await upsertTelegramBootstrapSession(workspaceDir, {
142687
+ conversationId,
142688
+ rawIdea: content,
142689
+ sourceRoute: { channel: "telegram", channelId: conversationId },
142690
+ status: "classifying"
142691
+ });
142692
+ classifyAndBootstrap(ctx, workspaceDir, conversationId, content).catch((err) => {
142693
+ logBootstrapWarning(ctx, `[telegram-bootstrap] LLM classify error: ${err instanceof Error ? err.message : String(err)}`);
142694
+ });
142695
+ }
142696
+ return;
142697
+ }
142522
142698
  const parsed = parseBootstrapRequest(content);
142523
142699
  const incomingRequest = {
142524
142700
  rawIdea: parsed.rawIdea,
@@ -142543,6 +142719,8 @@ function registerTelegramBootstrapHook(api, ctx) {
142543
142719
  ctx.logger.info(`[telegram-bootstrap] stale received session (expired) \u2014 restarting pipeline for conversation ${conversationId}`);
142544
142720
  }
142545
142721
  }
142722
+ const language = /\b(cria|crie|criar|construa|desenvolva|registre|novo projeto)\b/i.test(content) ? "pt" : "en";
142723
+ await sendTelegramText(ctx, conversationId, BOOTSTRAP_MESSAGES.ack[language]);
142546
142724
  const session = await upsertTelegramBootstrapSession(workspaceDir, {
142547
142725
  conversationId,
142548
142726
  ...incomingRequest,
@@ -142551,7 +142729,8 @@ function registerTelegramBootstrapHook(api, ctx) {
142551
142729
  channelId: conversationId
142552
142730
  },
142553
142731
  sourceChannel: "telegram",
142554
- status: "received"
142732
+ status: "received",
142733
+ language
142555
142734
  });
142556
142735
  if (!parsed.stackHint) {
142557
142736
  const pendingClarification = !parsed.projectName ? "stack_and_name" : "stack";
@@ -142560,9 +142739,10 @@ function registerTelegramBootstrapHook(api, ctx) {
142560
142739
  ...incomingRequest,
142561
142740
  sourceRoute: session.sourceRoute,
142562
142741
  status: "clarifying",
142563
- pendingClarification
142742
+ pendingClarification,
142743
+ language
142564
142744
  });
142565
- await sendTelegramText(ctx, conversationId, buildClarificationMessage(parsed, pendingClarification));
142745
+ await sendTelegramText(ctx, conversationId, buildClarificationMessage(parsed, pendingClarification, language));
142566
142746
  return;
142567
142747
  }
142568
142748
  continueBootstrap(ctx, conversationId, workspaceDir, incomingRequest, {