@wizdear/atlas-code 0.2.4 → 0.2.6

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.
Files changed (115) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-factory.d.ts +10 -5
  3. package/dist/agent-factory.d.ts.map +1 -1
  4. package/dist/agent-factory.js +50 -13
  5. package/dist/agent-factory.js.map +1 -1
  6. package/dist/cli.d.ts +7 -1
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +72 -16
  9. package/dist/cli.js.map +1 -1
  10. package/dist/discovery.d.ts +9 -2
  11. package/dist/discovery.d.ts.map +1 -1
  12. package/dist/discovery.js +4 -5
  13. package/dist/discovery.js.map +1 -1
  14. package/dist/extension.d.ts +9 -2
  15. package/dist/extension.d.ts.map +1 -1
  16. package/dist/extension.js +1103 -381
  17. package/dist/extension.js.map +1 -1
  18. package/dist/gate.d.ts +1 -1
  19. package/dist/gate.d.ts.map +1 -1
  20. package/dist/gate.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/orchestrator.d.ts +0 -4
  26. package/dist/orchestrator.d.ts.map +1 -1
  27. package/dist/orchestrator.js +0 -2
  28. package/dist/orchestrator.js.map +1 -1
  29. package/dist/pipeline-editor.d.ts +2 -0
  30. package/dist/pipeline-editor.d.ts.map +1 -1
  31. package/dist/pipeline-editor.js +36 -5
  32. package/dist/pipeline-editor.js.map +1 -1
  33. package/dist/pipeline.d.ts +2 -10
  34. package/dist/pipeline.d.ts.map +1 -1
  35. package/dist/pipeline.js +4 -6
  36. package/dist/pipeline.js.map +1 -1
  37. package/dist/planner.d.ts +9 -2
  38. package/dist/planner.d.ts.map +1 -1
  39. package/dist/planner.js +15 -11
  40. package/dist/planner.js.map +1 -1
  41. package/dist/roles/architect.d.ts +1 -1
  42. package/dist/roles/architect.d.ts.map +1 -1
  43. package/dist/roles/architect.js +1 -1
  44. package/dist/roles/architect.js.map +1 -1
  45. package/dist/roles/cicd.d.ts +1 -1
  46. package/dist/roles/cicd.d.ts.map +1 -1
  47. package/dist/roles/cicd.js +5 -0
  48. package/dist/roles/cicd.js.map +1 -1
  49. package/dist/roles/documenter.d.ts +1 -1
  50. package/dist/roles/documenter.d.ts.map +1 -1
  51. package/dist/roles/documenter.js +11 -0
  52. package/dist/roles/documenter.js.map +1 -1
  53. package/dist/roles/index.d.ts +1 -0
  54. package/dist/roles/index.d.ts.map +1 -1
  55. package/dist/roles/index.js +3 -0
  56. package/dist/roles/index.js.map +1 -1
  57. package/dist/roles/recover.d.ts +5 -0
  58. package/dist/roles/recover.d.ts.map +1 -0
  59. package/dist/roles/recover.js +82 -0
  60. package/dist/roles/recover.js.map +1 -0
  61. package/dist/roles/reviewer.d.ts +1 -1
  62. package/dist/roles/reviewer.d.ts.map +1 -1
  63. package/dist/roles/reviewer.js +7 -1
  64. package/dist/roles/reviewer.js.map +1 -1
  65. package/dist/roles/standards-enricher.d.ts +1 -1
  66. package/dist/roles/standards-enricher.d.ts.map +1 -1
  67. package/dist/roles/standards-enricher.js +8 -0
  68. package/dist/roles/standards-enricher.js.map +1 -1
  69. package/dist/roles/tester.d.ts +1 -1
  70. package/dist/roles/tester.d.ts.map +1 -1
  71. package/dist/roles/tester.js +7 -0
  72. package/dist/roles/tester.js.map +1 -1
  73. package/dist/router.d.ts.map +1 -1
  74. package/dist/router.js +6 -6
  75. package/dist/router.js.map +1 -1
  76. package/dist/standards.d.ts +37 -11
  77. package/dist/standards.d.ts.map +1 -1
  78. package/dist/standards.js +71 -89
  79. package/dist/standards.js.map +1 -1
  80. package/dist/step-executor.d.ts +15 -2
  81. package/dist/step-executor.d.ts.map +1 -1
  82. package/dist/step-executor.js +138 -30
  83. package/dist/step-executor.js.map +1 -1
  84. package/dist/store.d.ts +3 -10
  85. package/dist/store.d.ts.map +1 -1
  86. package/dist/store.js +45 -57
  87. package/dist/store.js.map +1 -1
  88. package/dist/system-architect.d.ts +9 -2
  89. package/dist/system-architect.d.ts.map +1 -1
  90. package/dist/system-architect.js +6 -10
  91. package/dist/system-architect.js.map +1 -1
  92. package/dist/telegram/bridge.d.ts +39 -0
  93. package/dist/telegram/bridge.d.ts.map +1 -0
  94. package/dist/telegram/bridge.js +380 -0
  95. package/dist/telegram/bridge.js.map +1 -0
  96. package/dist/telegram/formatter.d.ts +15 -0
  97. package/dist/telegram/formatter.d.ts.map +1 -0
  98. package/dist/telegram/formatter.js +86 -0
  99. package/dist/telegram/formatter.js.map +1 -0
  100. package/dist/telegram/renderer.d.ts +45 -0
  101. package/dist/telegram/renderer.d.ts.map +1 -0
  102. package/dist/telegram/renderer.js +150 -0
  103. package/dist/telegram/renderer.js.map +1 -0
  104. package/dist/telegram/telegram-api.d.ts +84 -0
  105. package/dist/telegram/telegram-api.d.ts.map +1 -0
  106. package/dist/telegram/telegram-api.js +134 -0
  107. package/dist/telegram/telegram-api.js.map +1 -0
  108. package/dist/types.d.ts +10 -1
  109. package/dist/types.d.ts.map +1 -1
  110. package/dist/types.js.map +1 -1
  111. package/dist/ui.d.ts +1 -1
  112. package/dist/ui.d.ts.map +1 -1
  113. package/dist/ui.js +2 -0
  114. package/dist/ui.js.map +1 -1
  115. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"system-architect.d.ts","sourceRoot":"","sources":["../src/system-architect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAItD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAI7C,0CAA0C;AAC1C,MAAM,WAAW,sBAAsB;IACtC,KAAK,EAAE,SAAS,CAAC;IACjB,MAAM,EAAE,UAAU,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,qEAAqE;IACrE,KAAK,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IACnB,iDAAiD;IACjD,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAC3C,mDAAmD;IACnD,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,qEAAqE;IACrE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACxC,0DAA0D;IAC1D,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED,yCAAyC;AACzC,MAAM,WAAW,qBAAqB;IACrC,8CAA8C;IAC9C,SAAS,EAAE,OAAO,CAAC;CACnB;AAID;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAuExG","sourcesContent":["import type { Agent, AgentEvent } from \"@mariozechner/pi-agent-core\";\nimport type { Model } from \"@mariozechner/pi-ai\";\nimport type { GetApiKeyFn } from \"./agent-factory.js\";\nimport { createRoleAgent, runAgent } from \"./agent-factory.js\";\nimport { getSystemPromptForRole } from \"./roles/index.js\";\nimport { extractArtifactContent } from \"./step-executor.js\";\nimport type { VibeStore } from \"./store.js\";\nimport type { VibeConfig } from \"./types.js\";\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** System Architect execution options. */\nexport interface SystemArchitectOptions {\n\tstore: VibeStore;\n\tconfig: VibeConfig;\n\tprojectRoot: string;\n\tgetApiKey?: GetApiKeyFn;\n\t/** Model to use. Uses the Agent's default model if not specified. */\n\tmodel?: Model<any>;\n\t/** Agent event callback for progress display. */\n\tonAgentEvent?: (event: AgentEvent) => void;\n\t/** Callback to relay agent messages externally. */\n\tonMessage?: (message: string) => void;\n\t/** User feedback on rejection. Re-generates with feedback applied. */\n\tfeedback?: string;\n\t/** Lightweight skill index. Included in the agent system prompt. */\n\tavailableSkills?: string;\n\t/** Callback invoked when the agent is created (for abort wiring). */\n\tonAgentCreated?: (agent: Agent) => void;\n\t/** Callback invoked when the agent finishes execution. */\n\tonAgentFinished?: () => void;\n}\n\n/** System Architect execution result. */\nexport interface SystemArchitectResult {\n\t/** Whether system-design.md was generated. */\n\tcompleted: boolean;\n}\n\n// ─── Runner ──────────────────────────────────────────────────────────────────\n\n/**\n * Runs the System Architect agent to generate system-design.md\n * based on requirements.md and project-context.md.\n *\n * 1. Load requirements.md and project-context.md from the store\n * 2. Optionally load existing system-design.md for iterative refinement\n * 3. Create systemArchitect agent\n * 4. Run the agent with a prompt (includes feedback if re-running)\n * 5. Extract system-design.md from the response\n * 6. Write to store\n */\nexport async function runSystemArchitect(options: SystemArchitectOptions): Promise<SystemArchitectResult> {\n\tconst { store, config, projectRoot, getApiKey, model, onAgentEvent, onMessage, feedback } = options;\n\n\t// 1. Load inputs\n\tlet requirements: string | undefined;\n\tif (await store.hasRequirements()) {\n\t\trequirements = await store.readRequirements();\n\t}\n\n\tlet projectContext: string | undefined;\n\tif (await store.hasProjectContext()) {\n\t\tprojectContext = await store.readProjectContext();\n\t}\n\n\tlet existingDesign: string | undefined;\n\tif (await store.hasSystemDesign()) {\n\t\texistingDesign = await store.readSystemDesign();\n\t}\n\n\t// 2. Build feature context (injected into system prompt)\n\tconst featureContext = buildSystemArchitectContext(requirements, projectContext, existingDesign);\n\n\t// 3. Create agent\n\tconst systemPrompt = getSystemPromptForRole(\"systemArchitect\");\n\tconst agent = await createRoleAgent({\n\t\trole: \"systemArchitect\",\n\t\tsystemPrompt,\n\t\tconfig,\n\t\tprojectRoot,\n\t\tmodel,\n\t\tfeatureContext: featureContext || undefined,\n\t\tgetApiKey,\n\t\tavailableSkills: options.availableSkills,\n\t});\n\n\toptions.onAgentCreated?.(agent);\n\n\t// 4. Build prompt and execute\n\ttry {\n\t\tlet prompt: string;\n\t\tif (feedback) {\n\t\t\tprompt =\n\t\t\t\t`The previous system design was rejected with the following feedback:\\n\\n${feedback}\\n\\n` +\n\t\t\t\t\"Please revise the system design based on this feedback.\\n\\n\" +\n\t\t\t\tbuildSystemArchitectPrompt(requirements, projectContext, existingDesign);\n\t\t} else {\n\t\t\tprompt = buildSystemArchitectPrompt(requirements, projectContext, existingDesign);\n\t\t}\n\n\t\tconst response = await runAgent(agent, prompt, onAgentEvent ? { onEvent: onAgentEvent } : undefined);\n\t\tonMessage?.(response);\n\n\t\t// 5. Extract system-design.md\n\t\tconst content = extractArtifactContent(response, \"system-design.md\", false);\n\t\tif (!content) {\n\t\t\treturn { completed: false };\n\t\t}\n\n\t\t// 6. Write to store\n\t\tawait store.writeSystemDesign(content);\n\n\t\treturn { completed: true };\n\t} catch (error) {\n\t\tconst msg = error instanceof Error ? error.message : String(error);\n\t\tif (msg.includes(\"aborted\")) {\n\t\t\treturn { completed: false };\n\t\t}\n\t\tthrow error;\n\t} finally {\n\t\toptions.onAgentFinished?.();\n\t}\n}\n\n// ─── Context & Prompt Builders ───────────────────────────────────────────────\n\nfunction buildSystemArchitectContext(requirements?: string, projectContext?: string, existingDesign?: string): string {\n\tconst parts: string[] = [];\n\n\tif (requirements) {\n\t\tparts.push(\"## Requirements\\n\");\n\t\tparts.push(requirements);\n\t}\n\n\tif (projectContext) {\n\t\tparts.push(\"## Project Context\\n\");\n\t\tparts.push(projectContext);\n\t}\n\n\tif (existingDesign) {\n\t\tparts.push(\"## Existing System Design (for refinement)\\n\");\n\t\tparts.push(existingDesign);\n\t}\n\n\treturn parts.join(\"\\n\\n\");\n}\n\nfunction buildSystemArchitectPrompt(requirements?: string, projectContext?: string, existingDesign?: string): string {\n\tconst parts: string[] = [];\n\n\tparts.push(\n\t\t\"Analyze the following inputs and produce a system-design.md document that establishes component boundaries, API contracts, shared data models, and common patterns.\",\n\t);\n\tparts.push(\"\");\n\n\tif (requirements) {\n\t\tparts.push(\"## Requirements Document\");\n\t\tparts.push(\"\");\n\t\tparts.push(requirements);\n\t\tparts.push(\"\");\n\t} else {\n\t\tparts.push(\"Note: No requirements.md found. Use the project context to infer system architecture.\");\n\t\tparts.push(\"\");\n\t}\n\n\tif (projectContext) {\n\t\tparts.push(\"## Project Context\");\n\t\tparts.push(\"\");\n\t\tparts.push(projectContext);\n\t\tparts.push(\"\");\n\t}\n\n\tif (existingDesign) {\n\t\tparts.push(\"## Existing System Design\");\n\t\tparts.push(\"\");\n\t\tparts.push(\n\t\t\t\"A previous system-design.md exists. Refine and update it based on the current requirements rather than starting from scratch.\",\n\t\t);\n\t\tparts.push(\"\");\n\t\tparts.push(existingDesign);\n\t\tparts.push(\"\");\n\t}\n\n\tparts.push(\"## Instructions\");\n\tparts.push(\"\");\n\tparts.push(\"1. Identify all system components and their boundaries\");\n\tparts.push(\"2. Define API contracts between components\");\n\tparts.push(\"3. Establish shared data models\");\n\tparts.push(\"4. Define common patterns (auth, error handling, logging)\");\n\tparts.push(\"5. Make infrastructure decisions based on the System Scope requirements\");\n\tparts.push(\"6. Note any decisions deferred to feature-level architects\");\n\tparts.push(\"7. Output the result in a ```system-design.md code block\");\n\n\treturn parts.join(\"\\n\");\n}\n"]}
1
+ {"version":3,"file":"system-architect.d.ts","sourceRoot":"","sources":["../src/system-architect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAItD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAI7C,0CAA0C;AAC1C,MAAM,WAAW,sBAAsB;IACtC,KAAK,EAAE,SAAS,CAAC;IACjB,MAAM,EAAE,UAAU,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,qEAAqE;IACrE,KAAK,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IACnB,iDAAiD;IACjD,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAC3C,mDAAmD;IACnD,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACxC,0DAA0D;IAC1D,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED,yCAAyC;AACzC,MAAM,WAAW,qBAAqB;IACrC,8CAA8C;IAC9C,SAAS,EAAE,OAAO,CAAC;IACnB,8BAA8B;IAC9B,KAAK,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACpH;AAID;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAuExG","sourcesContent":["import type { Agent, AgentEvent } from \"@mariozechner/pi-agent-core\";\nimport type { Model } from \"@mariozechner/pi-ai\";\nimport type { GetApiKeyFn } from \"./agent-factory.js\";\nimport { createRoleAgent, runAgent } from \"./agent-factory.js\";\nimport { getSystemPromptForRole } from \"./roles/index.js\";\nimport { aggregateUsage, extractArtifactContent } from \"./step-executor.js\";\nimport type { VibeStore } from \"./store.js\";\nimport type { VibeConfig } from \"./types.js\";\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** System Architect execution options. */\nexport interface SystemArchitectOptions {\n\tstore: VibeStore;\n\tconfig: VibeConfig;\n\tprojectRoot: string;\n\tgetApiKey?: GetApiKeyFn;\n\t/** Model to use. Uses the Agent's default model if not specified. */\n\tmodel?: Model<any>;\n\t/** Agent event callback for progress display. */\n\tonAgentEvent?: (event: AgentEvent) => void;\n\t/** Callback to relay agent messages externally. */\n\tonMessage?: (message: string) => void;\n\t/** User feedback on rejection. Re-generates with feedback applied. */\n\tfeedback?: string;\n\t/** Callback invoked when the agent is created (for abort wiring). */\n\tonAgentCreated?: (agent: Agent) => void;\n\t/** Callback invoked when the agent finishes execution. */\n\tonAgentFinished?: () => void;\n}\n\n/** System Architect execution result. */\nexport interface SystemArchitectResult {\n\t/** Whether system-design.md was generated. */\n\tcompleted: boolean;\n\t/** Aggregated token usage. */\n\tusage?: { input: number; output: number; cacheRead: number; cacheWrite: number; totalTokens: number; cost: number };\n}\n\n// ─── Runner ──────────────────────────────────────────────────────────────────\n\n/**\n * Runs the System Architect agent to generate system-design.md\n * based on requirements.md and project-context.md.\n *\n * 1. Load requirements.md and project-context.md from the store\n * 2. Optionally load existing system-design.md for iterative refinement\n * 3. Create systemArchitect agent\n * 4. Run the agent with a prompt (includes feedback if re-running)\n * 5. Extract system-design.md from the response\n * 6. Write to store\n */\nexport async function runSystemArchitect(options: SystemArchitectOptions): Promise<SystemArchitectResult> {\n\tconst { store, config, projectRoot, getApiKey, model, onAgentEvent, onMessage, feedback } = options;\n\n\t// 1. Load inputs\n\tlet requirements: string | undefined;\n\tif (await store.hasRequirements()) {\n\t\trequirements = await store.readRequirements();\n\t}\n\n\tlet projectContext: string | undefined;\n\tif (await store.hasProjectContext()) {\n\t\tprojectContext = await store.readProjectContext();\n\t}\n\n\tlet existingDesign: string | undefined;\n\tif (await store.hasSystemDesign()) {\n\t\texistingDesign = await store.readSystemDesign();\n\t}\n\n\t// 2. Build feature context (injected into system prompt)\n\tconst featureContext = buildSystemArchitectContext(requirements, existingDesign);\n\n\t// 3. Create agent\n\tconst systemPrompt = getSystemPromptForRole(\"systemArchitect\");\n\tconst agent = await createRoleAgent({\n\t\trole: \"systemArchitect\",\n\t\tsystemPrompt,\n\t\tconfig,\n\t\tprojectRoot,\n\t\tmodel,\n\t\tfeatureContext: featureContext || undefined,\n\t\tgetApiKey,\n\t});\n\n\toptions.onAgentCreated?.(agent);\n\n\t// 4. Build prompt and execute\n\ttry {\n\t\tlet prompt: string;\n\t\tif (feedback) {\n\t\t\tprompt =\n\t\t\t\t`The previous system design was rejected with the following feedback:\\n\\n${feedback}\\n\\n` +\n\t\t\t\t\"Please revise the system design based on this feedback.\\n\\n\" +\n\t\t\t\tbuildSystemArchitectPrompt(requirements, projectContext, existingDesign);\n\t\t} else {\n\t\t\tprompt = buildSystemArchitectPrompt(requirements, projectContext, existingDesign);\n\t\t}\n\n\t\tconst response = await runAgent(agent, prompt, onAgentEvent ? { onEvent: onAgentEvent } : undefined);\n\t\tconst usage = aggregateUsage(agent.state.messages);\n\t\tonMessage?.(response);\n\n\t\t// 5. Extract system-design.md\n\t\tconst content = extractArtifactContent(response, \"system-design.md\", false);\n\t\tif (!content) {\n\t\t\treturn { completed: false, usage };\n\t\t}\n\n\t\t// 6. Write to store\n\t\tawait store.writeSystemDesign(content);\n\n\t\treturn { completed: true, usage };\n\t} catch (error) {\n\t\tconst msg = error instanceof Error ? error.message : String(error);\n\t\tif (msg.includes(\"aborted\")) {\n\t\t\treturn { completed: false };\n\t\t}\n\t\tthrow error;\n\t} finally {\n\t\toptions.onAgentFinished?.();\n\t}\n}\n\n// ─── Context & Prompt Builders ───────────────────────────────────────────────\n\nfunction buildSystemArchitectContext(requirements?: string, existingDesign?: string): string {\n\tconst parts: string[] = [];\n\n\tif (requirements) {\n\t\tparts.push(\"## Requirements\\n\");\n\t\tparts.push(requirements);\n\t}\n\n\tif (existingDesign) {\n\t\tparts.push(\"## Existing System Design (for refinement)\\n\");\n\t\tparts.push(existingDesign);\n\t}\n\n\treturn parts.join(\"\\n\\n\");\n}\n\nfunction buildSystemArchitectPrompt(requirements?: string, projectContext?: string, existingDesign?: string): string {\n\tconst parts: string[] = [];\n\n\tparts.push(\n\t\t\"Analyze the following inputs and produce a system-design.md document that establishes component boundaries, API contracts, shared data models, and common patterns.\",\n\t);\n\tparts.push(\"\");\n\n\tif (requirements) {\n\t\tparts.push(\"## Requirements Document\");\n\t\tparts.push(\"\");\n\t\tparts.push(requirements);\n\t\tparts.push(\"\");\n\t} else {\n\t\tparts.push(\"Note: No requirements.md found. Use the project context to infer system architecture.\");\n\t\tparts.push(\"\");\n\t}\n\n\tif (projectContext) {\n\t\tparts.push(\"## Project Context\");\n\t\tparts.push(\"\");\n\t\tparts.push(projectContext);\n\t\tparts.push(\"\");\n\t}\n\n\tif (existingDesign) {\n\t\tparts.push(\"## Existing System Design\");\n\t\tparts.push(\"\");\n\t\tparts.push(\n\t\t\t\"A previous system-design.md exists. Refine and update it based on the current requirements rather than starting from scratch.\",\n\t\t);\n\t\tparts.push(\"\");\n\t\tparts.push(existingDesign);\n\t\tparts.push(\"\");\n\t}\n\n\tparts.push(\"## Instructions\");\n\tparts.push(\"\");\n\tparts.push(\"1. Identify all system components and their boundaries\");\n\tparts.push(\"2. Define API contracts between components\");\n\tparts.push(\"3. Establish shared data models\");\n\tparts.push(\"4. Define common patterns (auth, error handling, logging)\");\n\tparts.push(\"5. Make infrastructure decisions based on the System Scope requirements\");\n\tparts.push(\"6. Note any decisions deferred to feature-level architects\");\n\tparts.push(\"7. Output the result in a ```system-design.md code block\");\n\n\treturn parts.join(\"\\n\");\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import { createRoleAgent, runAgent } from "./agent-factory.js";
2
2
  import { getSystemPromptForRole } from "./roles/index.js";
3
- import { extractArtifactContent } from "./step-executor.js";
3
+ import { aggregateUsage, extractArtifactContent } from "./step-executor.js";
4
4
  // ─── Runner ──────────────────────────────────────────────────────────────────
5
5
  /**
6
6
  * Runs the System Architect agent to generate system-design.md
@@ -29,7 +29,7 @@ export async function runSystemArchitect(options) {
29
29
  existingDesign = await store.readSystemDesign();
30
30
  }
31
31
  // 2. Build feature context (injected into system prompt)
32
- const featureContext = buildSystemArchitectContext(requirements, projectContext, existingDesign);
32
+ const featureContext = buildSystemArchitectContext(requirements, existingDesign);
33
33
  // 3. Create agent
34
34
  const systemPrompt = getSystemPromptForRole("systemArchitect");
35
35
  const agent = await createRoleAgent({
@@ -40,7 +40,6 @@ export async function runSystemArchitect(options) {
40
40
  model,
41
41
  featureContext: featureContext || undefined,
42
42
  getApiKey,
43
- availableSkills: options.availableSkills,
44
43
  });
45
44
  options.onAgentCreated?.(agent);
46
45
  // 4. Build prompt and execute
@@ -56,15 +55,16 @@ export async function runSystemArchitect(options) {
56
55
  prompt = buildSystemArchitectPrompt(requirements, projectContext, existingDesign);
57
56
  }
58
57
  const response = await runAgent(agent, prompt, onAgentEvent ? { onEvent: onAgentEvent } : undefined);
58
+ const usage = aggregateUsage(agent.state.messages);
59
59
  onMessage?.(response);
60
60
  // 5. Extract system-design.md
61
61
  const content = extractArtifactContent(response, "system-design.md", false);
62
62
  if (!content) {
63
- return { completed: false };
63
+ return { completed: false, usage };
64
64
  }
65
65
  // 6. Write to store
66
66
  await store.writeSystemDesign(content);
67
- return { completed: true };
67
+ return { completed: true, usage };
68
68
  }
69
69
  catch (error) {
70
70
  const msg = error instanceof Error ? error.message : String(error);
@@ -78,16 +78,12 @@ export async function runSystemArchitect(options) {
78
78
  }
79
79
  }
80
80
  // ─── Context & Prompt Builders ───────────────────────────────────────────────
81
- function buildSystemArchitectContext(requirements, projectContext, existingDesign) {
81
+ function buildSystemArchitectContext(requirements, existingDesign) {
82
82
  const parts = [];
83
83
  if (requirements) {
84
84
  parts.push("## Requirements\n");
85
85
  parts.push(requirements);
86
86
  }
87
- if (projectContext) {
88
- parts.push("## Project Context\n");
89
- parts.push(projectContext);
90
- }
91
87
  if (existingDesign) {
92
88
  parts.push("## Existing System Design (for refinement)\n");
93
89
  parts.push(existingDesign);
@@ -1 +1 @@
1
- {"version":3,"file":"system-architect.js","sourceRoot":"","sources":["../src/system-architect.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Agent, AgentEvent } from \"@mariozechner/pi-agent-core\";\nimport type { Model } from \"@mariozechner/pi-ai\";\nimport type { GetApiKeyFn } from \"./agent-factory.js\";\nimport { createRoleAgent, runAgent } from \"./agent-factory.js\";\nimport { getSystemPromptForRole } from \"./roles/index.js\";\nimport { extractArtifactContent } from \"./step-executor.js\";\nimport type { VibeStore } from \"./store.js\";\nimport type { VibeConfig } from \"./types.js\";\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** System Architect execution options. */\nexport interface SystemArchitectOptions {\n\tstore: VibeStore;\n\tconfig: VibeConfig;\n\tprojectRoot: string;\n\tgetApiKey?: GetApiKeyFn;\n\t/** Model to use. Uses the Agent's default model if not specified. */\n\tmodel?: Model<any>;\n\t/** Agent event callback for progress display. */\n\tonAgentEvent?: (event: AgentEvent) => void;\n\t/** Callback to relay agent messages externally. */\n\tonMessage?: (message: string) => void;\n\t/** User feedback on rejection. Re-generates with feedback applied. */\n\tfeedback?: string;\n\t/** Lightweight skill index. Included in the agent system prompt. */\n\tavailableSkills?: string;\n\t/** Callback invoked when the agent is created (for abort wiring). */\n\tonAgentCreated?: (agent: Agent) => void;\n\t/** Callback invoked when the agent finishes execution. */\n\tonAgentFinished?: () => void;\n}\n\n/** System Architect execution result. */\nexport interface SystemArchitectResult {\n\t/** Whether system-design.md was generated. */\n\tcompleted: boolean;\n}\n\n// ─── Runner ──────────────────────────────────────────────────────────────────\n\n/**\n * Runs the System Architect agent to generate system-design.md\n * based on requirements.md and project-context.md.\n *\n * 1. Load requirements.md and project-context.md from the store\n * 2. Optionally load existing system-design.md for iterative refinement\n * 3. Create systemArchitect agent\n * 4. Run the agent with a prompt (includes feedback if re-running)\n * 5. Extract system-design.md from the response\n * 6. Write to store\n */\nexport async function runSystemArchitect(options: SystemArchitectOptions): Promise<SystemArchitectResult> {\n\tconst { store, config, projectRoot, getApiKey, model, onAgentEvent, onMessage, feedback } = options;\n\n\t// 1. Load inputs\n\tlet requirements: string | undefined;\n\tif (await store.hasRequirements()) {\n\t\trequirements = await store.readRequirements();\n\t}\n\n\tlet projectContext: string | undefined;\n\tif (await store.hasProjectContext()) {\n\t\tprojectContext = await store.readProjectContext();\n\t}\n\n\tlet existingDesign: string | undefined;\n\tif (await store.hasSystemDesign()) {\n\t\texistingDesign = await store.readSystemDesign();\n\t}\n\n\t// 2. Build feature context (injected into system prompt)\n\tconst featureContext = buildSystemArchitectContext(requirements, projectContext, existingDesign);\n\n\t// 3. Create agent\n\tconst systemPrompt = getSystemPromptForRole(\"systemArchitect\");\n\tconst agent = await createRoleAgent({\n\t\trole: \"systemArchitect\",\n\t\tsystemPrompt,\n\t\tconfig,\n\t\tprojectRoot,\n\t\tmodel,\n\t\tfeatureContext: featureContext || undefined,\n\t\tgetApiKey,\n\t\tavailableSkills: options.availableSkills,\n\t});\n\n\toptions.onAgentCreated?.(agent);\n\n\t// 4. Build prompt and execute\n\ttry {\n\t\tlet prompt: string;\n\t\tif (feedback) {\n\t\t\tprompt =\n\t\t\t\t`The previous system design was rejected with the following feedback:\\n\\n${feedback}\\n\\n` +\n\t\t\t\t\"Please revise the system design based on this feedback.\\n\\n\" +\n\t\t\t\tbuildSystemArchitectPrompt(requirements, projectContext, existingDesign);\n\t\t} else {\n\t\t\tprompt = buildSystemArchitectPrompt(requirements, projectContext, existingDesign);\n\t\t}\n\n\t\tconst response = await runAgent(agent, prompt, onAgentEvent ? { onEvent: onAgentEvent } : undefined);\n\t\tonMessage?.(response);\n\n\t\t// 5. Extract system-design.md\n\t\tconst content = extractArtifactContent(response, \"system-design.md\", false);\n\t\tif (!content) {\n\t\t\treturn { completed: false };\n\t\t}\n\n\t\t// 6. Write to store\n\t\tawait store.writeSystemDesign(content);\n\n\t\treturn { completed: true };\n\t} catch (error) {\n\t\tconst msg = error instanceof Error ? error.message : String(error);\n\t\tif (msg.includes(\"aborted\")) {\n\t\t\treturn { completed: false };\n\t\t}\n\t\tthrow error;\n\t} finally {\n\t\toptions.onAgentFinished?.();\n\t}\n}\n\n// ─── Context & Prompt Builders ───────────────────────────────────────────────\n\nfunction buildSystemArchitectContext(requirements?: string, projectContext?: string, existingDesign?: string): string {\n\tconst parts: string[] = [];\n\n\tif (requirements) {\n\t\tparts.push(\"## Requirements\\n\");\n\t\tparts.push(requirements);\n\t}\n\n\tif (projectContext) {\n\t\tparts.push(\"## Project Context\\n\");\n\t\tparts.push(projectContext);\n\t}\n\n\tif (existingDesign) {\n\t\tparts.push(\"## Existing System Design (for refinement)\\n\");\n\t\tparts.push(existingDesign);\n\t}\n\n\treturn parts.join(\"\\n\\n\");\n}\n\nfunction buildSystemArchitectPrompt(requirements?: string, projectContext?: string, existingDesign?: string): string {\n\tconst parts: string[] = [];\n\n\tparts.push(\n\t\t\"Analyze the following inputs and produce a system-design.md document that establishes component boundaries, API contracts, shared data models, and common patterns.\",\n\t);\n\tparts.push(\"\");\n\n\tif (requirements) {\n\t\tparts.push(\"## Requirements Document\");\n\t\tparts.push(\"\");\n\t\tparts.push(requirements);\n\t\tparts.push(\"\");\n\t} else {\n\t\tparts.push(\"Note: No requirements.md found. Use the project context to infer system architecture.\");\n\t\tparts.push(\"\");\n\t}\n\n\tif (projectContext) {\n\t\tparts.push(\"## Project Context\");\n\t\tparts.push(\"\");\n\t\tparts.push(projectContext);\n\t\tparts.push(\"\");\n\t}\n\n\tif (existingDesign) {\n\t\tparts.push(\"## Existing System Design\");\n\t\tparts.push(\"\");\n\t\tparts.push(\n\t\t\t\"A previous system-design.md exists. Refine and update it based on the current requirements rather than starting from scratch.\",\n\t\t);\n\t\tparts.push(\"\");\n\t\tparts.push(existingDesign);\n\t\tparts.push(\"\");\n\t}\n\n\tparts.push(\"## Instructions\");\n\tparts.push(\"\");\n\tparts.push(\"1. Identify all system components and their boundaries\");\n\tparts.push(\"2. Define API contracts between components\");\n\tparts.push(\"3. Establish shared data models\");\n\tparts.push(\"4. Define common patterns (auth, error handling, logging)\");\n\tparts.push(\"5. Make infrastructure decisions based on the System Scope requirements\");\n\tparts.push(\"6. Note any decisions deferred to feature-level architects\");\n\tparts.push(\"7. Output the result in a ```system-design.md code block\");\n\n\treturn parts.join(\"\\n\");\n}\n"]}
1
+ {"version":3,"file":"system-architect.js","sourceRoot":"","sources":["../src/system-architect.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Agent, AgentEvent } from \"@mariozechner/pi-agent-core\";\nimport type { Model } from \"@mariozechner/pi-ai\";\nimport type { GetApiKeyFn } from \"./agent-factory.js\";\nimport { createRoleAgent, runAgent } from \"./agent-factory.js\";\nimport { getSystemPromptForRole } from \"./roles/index.js\";\nimport { aggregateUsage, extractArtifactContent } from \"./step-executor.js\";\nimport type { VibeStore } from \"./store.js\";\nimport type { VibeConfig } from \"./types.js\";\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** System Architect execution options. */\nexport interface SystemArchitectOptions {\n\tstore: VibeStore;\n\tconfig: VibeConfig;\n\tprojectRoot: string;\n\tgetApiKey?: GetApiKeyFn;\n\t/** Model to use. Uses the Agent's default model if not specified. */\n\tmodel?: Model<any>;\n\t/** Agent event callback for progress display. */\n\tonAgentEvent?: (event: AgentEvent) => void;\n\t/** Callback to relay agent messages externally. */\n\tonMessage?: (message: string) => void;\n\t/** User feedback on rejection. Re-generates with feedback applied. */\n\tfeedback?: string;\n\t/** Callback invoked when the agent is created (for abort wiring). */\n\tonAgentCreated?: (agent: Agent) => void;\n\t/** Callback invoked when the agent finishes execution. */\n\tonAgentFinished?: () => void;\n}\n\n/** System Architect execution result. */\nexport interface SystemArchitectResult {\n\t/** Whether system-design.md was generated. */\n\tcompleted: boolean;\n\t/** Aggregated token usage. */\n\tusage?: { input: number; output: number; cacheRead: number; cacheWrite: number; totalTokens: number; cost: number };\n}\n\n// ─── Runner ──────────────────────────────────────────────────────────────────\n\n/**\n * Runs the System Architect agent to generate system-design.md\n * based on requirements.md and project-context.md.\n *\n * 1. Load requirements.md and project-context.md from the store\n * 2. Optionally load existing system-design.md for iterative refinement\n * 3. Create systemArchitect agent\n * 4. Run the agent with a prompt (includes feedback if re-running)\n * 5. Extract system-design.md from the response\n * 6. Write to store\n */\nexport async function runSystemArchitect(options: SystemArchitectOptions): Promise<SystemArchitectResult> {\n\tconst { store, config, projectRoot, getApiKey, model, onAgentEvent, onMessage, feedback } = options;\n\n\t// 1. Load inputs\n\tlet requirements: string | undefined;\n\tif (await store.hasRequirements()) {\n\t\trequirements = await store.readRequirements();\n\t}\n\n\tlet projectContext: string | undefined;\n\tif (await store.hasProjectContext()) {\n\t\tprojectContext = await store.readProjectContext();\n\t}\n\n\tlet existingDesign: string | undefined;\n\tif (await store.hasSystemDesign()) {\n\t\texistingDesign = await store.readSystemDesign();\n\t}\n\n\t// 2. Build feature context (injected into system prompt)\n\tconst featureContext = buildSystemArchitectContext(requirements, existingDesign);\n\n\t// 3. Create agent\n\tconst systemPrompt = getSystemPromptForRole(\"systemArchitect\");\n\tconst agent = await createRoleAgent({\n\t\trole: \"systemArchitect\",\n\t\tsystemPrompt,\n\t\tconfig,\n\t\tprojectRoot,\n\t\tmodel,\n\t\tfeatureContext: featureContext || undefined,\n\t\tgetApiKey,\n\t});\n\n\toptions.onAgentCreated?.(agent);\n\n\t// 4. Build prompt and execute\n\ttry {\n\t\tlet prompt: string;\n\t\tif (feedback) {\n\t\t\tprompt =\n\t\t\t\t`The previous system design was rejected with the following feedback:\\n\\n${feedback}\\n\\n` +\n\t\t\t\t\"Please revise the system design based on this feedback.\\n\\n\" +\n\t\t\t\tbuildSystemArchitectPrompt(requirements, projectContext, existingDesign);\n\t\t} else {\n\t\t\tprompt = buildSystemArchitectPrompt(requirements, projectContext, existingDesign);\n\t\t}\n\n\t\tconst response = await runAgent(agent, prompt, onAgentEvent ? { onEvent: onAgentEvent } : undefined);\n\t\tconst usage = aggregateUsage(agent.state.messages);\n\t\tonMessage?.(response);\n\n\t\t// 5. Extract system-design.md\n\t\tconst content = extractArtifactContent(response, \"system-design.md\", false);\n\t\tif (!content) {\n\t\t\treturn { completed: false, usage };\n\t\t}\n\n\t\t// 6. Write to store\n\t\tawait store.writeSystemDesign(content);\n\n\t\treturn { completed: true, usage };\n\t} catch (error) {\n\t\tconst msg = error instanceof Error ? error.message : String(error);\n\t\tif (msg.includes(\"aborted\")) {\n\t\t\treturn { completed: false };\n\t\t}\n\t\tthrow error;\n\t} finally {\n\t\toptions.onAgentFinished?.();\n\t}\n}\n\n// ─── Context & Prompt Builders ───────────────────────────────────────────────\n\nfunction buildSystemArchitectContext(requirements?: string, existingDesign?: string): string {\n\tconst parts: string[] = [];\n\n\tif (requirements) {\n\t\tparts.push(\"## Requirements\\n\");\n\t\tparts.push(requirements);\n\t}\n\n\tif (existingDesign) {\n\t\tparts.push(\"## Existing System Design (for refinement)\\n\");\n\t\tparts.push(existingDesign);\n\t}\n\n\treturn parts.join(\"\\n\\n\");\n}\n\nfunction buildSystemArchitectPrompt(requirements?: string, projectContext?: string, existingDesign?: string): string {\n\tconst parts: string[] = [];\n\n\tparts.push(\n\t\t\"Analyze the following inputs and produce a system-design.md document that establishes component boundaries, API contracts, shared data models, and common patterns.\",\n\t);\n\tparts.push(\"\");\n\n\tif (requirements) {\n\t\tparts.push(\"## Requirements Document\");\n\t\tparts.push(\"\");\n\t\tparts.push(requirements);\n\t\tparts.push(\"\");\n\t} else {\n\t\tparts.push(\"Note: No requirements.md found. Use the project context to infer system architecture.\");\n\t\tparts.push(\"\");\n\t}\n\n\tif (projectContext) {\n\t\tparts.push(\"## Project Context\");\n\t\tparts.push(\"\");\n\t\tparts.push(projectContext);\n\t\tparts.push(\"\");\n\t}\n\n\tif (existingDesign) {\n\t\tparts.push(\"## Existing System Design\");\n\t\tparts.push(\"\");\n\t\tparts.push(\n\t\t\t\"A previous system-design.md exists. Refine and update it based on the current requirements rather than starting from scratch.\",\n\t\t);\n\t\tparts.push(\"\");\n\t\tparts.push(existingDesign);\n\t\tparts.push(\"\");\n\t}\n\n\tparts.push(\"## Instructions\");\n\tparts.push(\"\");\n\tparts.push(\"1. Identify all system components and their boundaries\");\n\tparts.push(\"2. Define API contracts between components\");\n\tparts.push(\"3. Establish shared data models\");\n\tparts.push(\"4. Define common patterns (auth, error handling, logging)\");\n\tparts.push(\"5. Make infrastructure decisions based on the System Scope requirements\");\n\tparts.push(\"6. Note any decisions deferred to feature-level architects\");\n\tparts.push(\"7. Output the result in a ```system-design.md code block\");\n\n\treturn parts.join(\"\\n\");\n}\n"]}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Telegram bridge — mirrors TUI state to Telegram and accepts simple commands.
3
+ * Activated automatically when token is available (env var or .vibe/config.json).
4
+ * Chat ID is auto-discovered from the first incoming message if not configured.
5
+ */
6
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
7
+ interface TelegramBridgeConfig {
8
+ token: string;
9
+ chatId?: string;
10
+ }
11
+ /**
12
+ * Resolve Telegram config from env vars and .vibe/config.json.
13
+ * Priority: env var > config file.
14
+ */
15
+ export declare function resolveConfig(vibeDir: string): TelegramBridgeConfig | undefined;
16
+ /** Registered command entry for Telegram dispatch. */
17
+ export interface TelegramCommand {
18
+ name: string;
19
+ description?: string;
20
+ handler: (args: string, ctx: ExtensionCommandContext) => Promise<void>;
21
+ /** Subcommands rendered as separate entries in Telegram bot menu (e.g., vibe_resume). */
22
+ subcommands?: Array<{
23
+ name: string;
24
+ description: string;
25
+ }>;
26
+ }
27
+ /**
28
+ * Create a bridge manager that supports dynamic mid-session activation.
29
+ * Call `tryStart()` on session_start and agent_end. Call `stop()` on session_shutdown.
30
+ *
31
+ * @param commands Shared array of commands. Items can be added after creation;
32
+ * the polling loop reads from this array by reference on every message.
33
+ */
34
+ export declare function createTelegramBridgeManager(pi: ExtensionAPI, ctx: ExtensionContext, vibeDir: string, commands?: TelegramCommand[]): {
35
+ tryStart: () => void;
36
+ stop: () => void;
37
+ };
38
+ export {};
39
+ //# sourceMappingURL=bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../../src/telegram/bridge.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AA6B7G,UAAU,oBAAoB;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,oBAAoB,GAAG,SAAS,CAqB/E;AAoBD,sDAAsD;AACtD,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,uBAAuB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,yFAAyF;IACzF,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC3D;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAC1C,EAAE,EAAE,YAAY,EAChB,GAAG,EAAE,gBAAgB,EACrB,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,eAAe,EAAO,GAC9B;IAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,IAAI,CAAA;CAAE,CAoB5C","sourcesContent":["/**\n * Telegram bridge — mirrors TUI state to Telegram and accepts simple commands.\n * Activated automatically when token is available (env var or .vibe/config.json).\n * Chat ID is auto-discovered from the first incoming message if not configured.\n */\n\nimport { readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nimport type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from \"@mariozechner/pi-coding-agent\";\n\nimport { formatToolLabel } from \"./formatter.js\";\nimport type { StepDisplay } from \"./renderer.js\";\nimport { ResponseRenderer } from \"./renderer.js\";\nimport { TelegramAPI } from \"./telegram-api.js\";\n\n// ─── Vibe event shapes (local, not exported) ────────────────────────────────\n\ninterface VibePipelineEvent {\n\ttype: string;\n\tfeatureId: string;\n\tstep?: { agent: string; action: string };\n\tdata?: Record<string, unknown>;\n}\n\ninterface VibeActivityEvent {\n\trole: string;\n\ttext: string;\n\ttype: string;\n}\n\ninterface VibeGateEvent {\n\tfeatureId: string;\n\tsummary: string;\n}\n\n// ─── Config resolution ───────────────────────────────────────────────────────\n\ninterface TelegramBridgeConfig {\n\ttoken: string;\n\tchatId?: string;\n}\n\n/**\n * Resolve Telegram config from env vars and .vibe/config.json.\n * Priority: env var > config file.\n */\nexport function resolveConfig(vibeDir: string): TelegramBridgeConfig | undefined {\n\tlet fileToken: string | undefined;\n\tlet fileChatId: string | undefined;\n\n\ttry {\n\t\tconst configPath = join(vibeDir, \"config.json\");\n\t\tconst raw = JSON.parse(readFileSync(configPath, \"utf-8\")) as Record<string, unknown>;\n\t\tconst tg = raw.telegram as Record<string, unknown> | undefined;\n\t\tif (tg) {\n\t\t\tfileToken = typeof tg.token === \"string\" ? tg.token : undefined;\n\t\t\tfileChatId = typeof tg.chatId === \"string\" ? tg.chatId : undefined;\n\t\t}\n\t} catch {\n\t\t// Config file may not exist\n\t}\n\n\tconst token = process.env.VIBE_TELEGRAM_TOKEN ?? fileToken;\n\tif (!token) return undefined;\n\n\tconst chatId = process.env.VIBE_TELEGRAM_CHAT_ID ?? fileChatId;\n\treturn { token, chatId: chatId || undefined };\n}\n\n/** Save discovered chat ID to .vibe/config.json. */\nfunction saveDiscoveredChatId(vibeDir: string, chatId: string): void {\n\tconst configPath = join(vibeDir, \"config.json\");\n\tlet config: Record<string, unknown> = {};\n\ttry {\n\t\tconfig = JSON.parse(readFileSync(configPath, \"utf-8\")) as Record<string, unknown>;\n\t} catch {\n\t\t// File may not exist\n\t}\n\tif (!config.telegram || typeof config.telegram !== \"object\") {\n\t\tconfig.telegram = {};\n\t}\n\t(config.telegram as Record<string, unknown>).chatId = chatId;\n\twriteFileSync(configPath, `${JSON.stringify(config, null, \"\\t\")}\\n`, \"utf-8\");\n}\n\n// ─── Bridge Manager ──────────────────────────────────────────────────────────\n\n/** Registered command entry for Telegram dispatch. */\nexport interface TelegramCommand {\n\tname: string;\n\tdescription?: string;\n\thandler: (args: string, ctx: ExtensionCommandContext) => Promise<void>;\n\t/** Subcommands rendered as separate entries in Telegram bot menu (e.g., vibe_resume). */\n\tsubcommands?: Array<{ name: string; description: string }>;\n}\n\n/**\n * Create a bridge manager that supports dynamic mid-session activation.\n * Call `tryStart()` on session_start and agent_end. Call `stop()` on session_shutdown.\n *\n * @param commands Shared array of commands. Items can be added after creation;\n * the polling loop reads from this array by reference on every message.\n */\nexport function createTelegramBridgeManager(\n\tpi: ExtensionAPI,\n\tctx: ExtensionContext,\n\tvibeDir: string,\n\tcommands: TelegramCommand[] = [],\n): { tryStart: () => void; stop: () => void } {\n\tlet cleanup: (() => void) | undefined;\n\tlet started = false;\n\n\tfunction tryStart(): void {\n\t\tif (started) return;\n\t\tconst config = resolveConfig(vibeDir);\n\t\tif (!config) return;\n\n\t\tcleanup = startBridge(pi, ctx, config, vibeDir, commands);\n\t\tstarted = true;\n\t}\n\n\tfunction stop(): void {\n\t\tcleanup?.();\n\t\tcleanup = undefined;\n\t\tstarted = false;\n\t}\n\n\treturn { tryStart, stop };\n}\n\n// ─── Bridge core ─────────────────────────────────────────────────────────────\n\nfunction startBridge(\n\tpi: ExtensionAPI,\n\tctx: ExtensionContext,\n\tconfig: TelegramBridgeConfig,\n\tvibeDir: string,\n\tcommands: TelegramCommand[],\n): () => void {\n\tconst tg = new TelegramAPI(config.token, config.chatId);\n\tconst renderer = new ResponseRenderer(tg);\n\tconst abortController = new AbortController();\n\tconst unsubs: Array<() => void> = [];\n\n\t// Pipeline/gate state tracking — prevents Telegram input during vibe workflows\n\t// and preserves renderer state between pipeline steps.\n\tlet pipelineActive = false;\n\tlet gateWaiting = false;\n\n\t// 1. Agent event mirroring\n\tpi.on(\"message_update\", (event) => {\n\t\tif (event.assistantMessageEvent.type === \"text_delta\") {\n\t\t\trenderer.appendText(event.assistantMessageEvent.delta);\n\t\t}\n\t});\n\n\tpi.on(\"tool_execution_start\", (event) => {\n\t\tconst label = formatToolLabel(event.toolName, event.args as Record<string, unknown>);\n\t\trenderer.appendToolInfo(`\\u2192 ${label}`);\n\t});\n\n\tpi.on(\"agent_end\", () => {\n\t\t// During pipeline execution, don't finalize — the pipeline events manage renderer lifecycle.\n\t\t// Finalizing here would reset the step table between steps.\n\t\tif (!pipelineActive) {\n\t\t\trenderer.finalize().catch(() => {});\n\t\t}\n\t});\n\n\t// 1b. Vibe custom messages (agent responses, milestones, errors, etc.)\n\t// pi.sendMessage() → sendCustomMessage() only fires _emit() (TUI listeners),\n\t// not _emitExtensionEvent(), so pi.on(\"message_end\") never fires.\n\t// Instead, sendVibeMessage() emits vibe:message on the EventBus.\n\tunsubs.push(\n\t\tpi.events.on(\"vibe:message\", (raw: unknown) => {\n\t\t\tconst event = raw as {\n\t\t\t\tcontent: string;\n\t\t\t\tcategory: string;\n\t\t\t\tfullText?: string;\n\t\t\t};\n\n\t\t\t// Skip categories already handled by pipeline/gate event handlers\n\t\t\tif (event.category === \"gate\") return;\n\t\t\tif (!event.content) return;\n\n\t\t\tconst icons: Record<string, string> = {\n\t\t\t\tmilestone: \"\\u2728\",\n\t\t\t\terror: \"\\u274C\",\n\t\t\t\twarning: \"\\u26A0\\uFE0F\",\n\t\t\t\tcommand: \"\\u25B6\",\n\t\t\t\tevent: \"\\u{1F4AC}\",\n\t\t\t\t\"agent-response\": \"\\u{1F916}\",\n\t\t\t\t\"agent-text\": \"\\u270F\\uFE0F\",\n\t\t\t};\n\t\t\tconst icon = icons[event.category] ?? \"\\u{1F4AC}\";\n\n\t\t\t// For agent-text, show the first line of fullText instead of the generic\n\t\t\t// \"[role] ✎ (N lines)\" summary. This gives meaningful context (e.g., file\n\t\t\t// being worked on) instead of identical-looking messages.\n\t\t\tlet displayText = event.content;\n\t\t\tif (event.category === \"agent-text\" && event.fullText) {\n\t\t\t\tconst firstLine = event.fullText.split(\"\\n\")[0].trim();\n\t\t\t\tif (firstLine) {\n\t\t\t\t\tdisplayText = firstLine;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Send as plain text to avoid Markdown parse failures from LLM output\n\t\t\ttg.sendMessage(`${icon} ${displayText}`, { parse_mode: null }).catch(() => {});\n\t\t}),\n\t);\n\n\t// 2. Vibe event mirroring\n\tunsubs.push(\n\t\tpi.events.on(\"vibe:pipeline\", (raw: unknown) => {\n\t\t\tconst event = raw as VibePipelineEvent;\n\t\t\tif (event.type === \"feature_start\") {\n\t\t\t\tpipelineActive = true;\n\t\t\t} else if (\n\t\t\t\tevent.type === \"pipeline_complete\" ||\n\t\t\t\tevent.type === \"feature_complete\" ||\n\t\t\t\tevent.type === \"orchestration_complete\" ||\n\t\t\t\tevent.type === \"feature_aborted\"\n\t\t\t) {\n\t\t\t\tpipelineActive = false;\n\t\t\t\tgateWaiting = false;\n\t\t\t}\n\t\t\thandlePipelineEvent(event, renderer);\n\t\t}),\n\t\tpi.events.on(\"vibe:activity\", (raw: unknown) => {\n\t\t\tconst event = raw as VibeActivityEvent;\n\t\t\trenderer.addActivity(event.text);\n\t\t}),\n\t\tpi.events.on(\"vibe:gate\", (raw: unknown) => {\n\t\t\tconst event = raw as VibeGateEvent;\n\t\t\tgateWaiting = true;\n\t\t\tconst keyboard = {\n\t\t\t\tinline_keyboard: [\n\t\t\t\t\t[\n\t\t\t\t\t\t{ text: \"\\u2705 Approve\", callback_data: \"Approve\" },\n\t\t\t\t\t\t{ text: \"\\u274C Reject\", callback_data: \"Reject\" },\n\t\t\t\t\t],\n\t\t\t\t\t[\n\t\t\t\t\t\t{ text: \"\\u23ED Skip\", callback_data: \"Skip\" },\n\t\t\t\t\t\t{ text: \"\\u26D4 Abort\", callback_data: \"Abort\" },\n\t\t\t\t\t],\n\t\t\t\t],\n\t\t\t};\n\t\t\ttg.sendMessage(`\\u23F3 Gate: ${event.summary}`, { reply_markup: keyboard }).catch(() => {});\n\t\t}),\n\t);\n\n\t// Reset gate flag when pipeline resumes after gate approval\n\tunsubs.push(\n\t\tpi.events.on(\"vibe:pipeline\", (raw: unknown) => {\n\t\t\tconst event = raw as VibePipelineEvent;\n\t\t\tif (event.type === \"step_start\") {\n\t\t\t\tgateWaiting = false;\n\t\t\t}\n\t\t}),\n\t);\n\n\t// 3. Polling loop (fire-and-forget)\n\tstartPolling(tg, pi, ctx, abortController.signal, vibeDir, {\n\t\tisVibeBusy: () => pipelineActive || gateWaiting,\n\t\tisGateWaiting: () => gateWaiting,\n\t\tclearGateWaiting: () => {\n\t\t\tgateWaiting = false;\n\t\t},\n\t\tcommands,\n\t});\n\n\t// 4. Register bot menu commands (including subcommands as separate entries)\n\tconst menuCommands: Array<{ command: string; description: string }> = [\n\t\t{ command: \"stop\", description: \"Stop current execution\" },\n\t];\n\tfor (const cmd of commands) {\n\t\tmenuCommands.push({ command: cmd.name, description: cmd.description ?? `/${cmd.name}` });\n\t\tif (cmd.subcommands) {\n\t\t\tfor (const sub of cmd.subcommands) {\n\t\t\t\t// Telegram command names: lowercase, digits, underscores only\n\t\t\t\tmenuCommands.push({ command: `${cmd.name}_${sub.name}`, description: sub.description });\n\t\t\t}\n\t\t}\n\t}\n\ttg.setMyCommands(menuCommands).catch(() => {});\n\n\t// 5. Startup\n\tif (tg.hasChatId()) {\n\t\tconst model = ctx.model;\n\t\tconst startupMsg = model\n\t\t\t? `Telegram bridge active. Model: ${model.provider}/${model.id}`\n\t\t\t: \"Telegram bridge active. No model.\";\n\t\ttg.sendMessage(startupMsg).catch(() => {});\n\t} else {\n\t\tctx.ui.notify(\"[Telegram] Waiting for first message from your bot\", \"info\");\n\t}\n\n\t// Return cleanup function\n\treturn () => {\n\t\tabortController.abort();\n\t\tfor (const unsub of unsubs) unsub();\n\t};\n}\n\n// ─── Polling ─────────────────────────────────────────────────────────────────\n\nfunction sleep(ms: number): Promise<void> {\n\treturn new Promise((resolve) => setTimeout(resolve, ms));\n}\n\ninterface PollingState {\n\tisVibeBusy: () => boolean;\n\tisGateWaiting: () => boolean;\n\tclearGateWaiting: () => void;\n\tcommands: TelegramCommand[];\n}\n\nasync function startPolling(\n\ttg: TelegramAPI,\n\tpi: ExtensionAPI,\n\tctx: ExtensionContext,\n\tsignal: AbortSignal,\n\tvibeDir: string,\n\tstate: PollingState,\n): Promise<void> {\n\twhile (!signal.aborted) {\n\t\ttry {\n\t\t\tconst updates = await tg.getUpdates();\n\t\t\tfor (const update of updates) {\n\t\t\t\tif (signal.aborted) break;\n\n\t\t\t\t// Handle callback queries (gate button clicks)\n\t\t\t\tif (update.callback_query) {\n\t\t\t\t\tconst data = update.callback_query.data ?? \"\";\n\t\t\t\t\ttg.answerCallbackQuery(update.callback_query.id).catch(() => {});\n\t\t\t\t\tif (state.isGateWaiting()) {\n\t\t\t\t\t\tpi.events.emit(\"vibe:gate_response\", { choice: data });\n\t\t\t\t\t\tstate.clearGateWaiting();\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Auto-discover chat ID from first incoming message\n\t\t\t\tif (!tg.hasChatId() && update.message?.chat.id) {\n\t\t\t\t\tconst discoveredId = String(update.message.chat.id);\n\t\t\t\t\ttg.setChatId(discoveredId);\n\t\t\t\t\tsaveDiscoveredChatId(vibeDir, discoveredId);\n\t\t\t\t\tconst model = ctx.model;\n\t\t\t\t\tconst msg = model\n\t\t\t\t\t\t? `Telegram bridge connected. Model: ${model.provider}/${model.id}`\n\t\t\t\t\t\t: \"Telegram bridge connected.\";\n\t\t\t\t\tawait tg.sendMessage(msg).catch(() => {});\n\t\t\t\t\tctx.ui.notify(\"[Telegram] Connected\", \"info\");\n\t\t\t\t\tcontinue; // Skip processing discovery message as a command\n\t\t\t\t}\n\n\t\t\t\tconst text = update.message?.text;\n\t\t\t\tif (!text) continue;\n\n\t\t\t\tif (text === \"/stop\") {\n\t\t\t\t\tctx.abort();\n\t\t\t\t\tawait tg.sendMessage(\"\\u23F8 Stopped.\").catch(() => {});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Block input during pipeline execution or gate waiting\n\t\t\t\tif (state.isVibeBusy()) {\n\t\t\t\t\tawait tg.sendMessage(\"\\u23F3 Pipeline running. Wait or /stop.\").catch(() => {});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (ctx.isIdle()) {\n\t\t\t\t\t// Try to dispatch as extension command first.\n\t\t\t\t\t// Fire-and-forget: command handlers may block on gates/UI,\n\t\t\t\t\t// and the polling loop must keep running to receive callback queries.\n\t\t\t\t\tif (text.startsWith(\"/\") && tryDispatchCommandSync(text, state.commands, ctx)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tpi.sendUserMessage(text);\n\t\t\t\t} else {\n\t\t\t\t\tawait tg.sendMessage(\"\\u23F3 Busy. Wait or /stop.\").catch(() => {});\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\tif (signal.aborted) break;\n\t\t\tawait sleep(5000);\n\t\t}\n\t}\n}\n\n// ─── Command dispatch ────────────────────────────────────────────────────────\n\n/**\n * Try to dispatch a slash command to a registered handler.\n * Handles both direct commands (/vibe resume) and Telegram-style\n * subcommands (/vibe_resume) by mapping underscores back to spaces.\n *\n * Returns true if the command was matched. The handler runs as\n * fire-and-forget so the polling loop is never blocked (handlers\n * may await gates/UI dialogs indefinitely).\n */\nfunction tryDispatchCommandSync(text: string, commands: TelegramCommand[], ctx: ExtensionContext): boolean {\n\t// Strip leading / and optional @BotName suffix (Telegram adds this in groups)\n\tconst raw = text.slice(1).split(\"@\")[0];\n\tconst spaceIndex = raw.indexOf(\" \");\n\tconst fullName = spaceIndex === -1 ? raw : raw.slice(0, spaceIndex);\n\tconst trailingArgs = spaceIndex === -1 ? \"\" : raw.slice(spaceIndex + 1);\n\n\t// 1. Direct match: /vibe resume → command=\"vibe\", args=\"resume\"\n\tconst directMatch = commands.find((c) => c.name === fullName);\n\tif (directMatch) {\n\t\tdirectMatch.handler(trailingArgs, ctx as unknown as ExtensionCommandContext).catch(() => {});\n\t\treturn true;\n\t}\n\n\t// 2. Subcommand match: /vibe_resume → command=\"vibe\", args=\"resume\"\n\tfor (const cmd of commands) {\n\t\tif (!cmd.subcommands) continue;\n\t\tconst prefix = `${cmd.name}_`;\n\t\tif (fullName.startsWith(prefix)) {\n\t\t\tconst subName = fullName.slice(prefix.length);\n\t\t\tif (cmd.subcommands.some((s) => s.name === subName)) {\n\t\t\t\tconst combinedArgs = trailingArgs ? `${subName} ${trailingArgs}` : subName;\n\t\t\t\tcmd.handler(combinedArgs, ctx as unknown as ExtensionCommandContext).catch(() => {});\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false;\n}\n\n// ─── Pipeline event handler ─────────────────────────────────────────────────\n\nfunction handlePipelineEvent(event: VibePipelineEvent, renderer: ResponseRenderer): void {\n\tswitch (event.type) {\n\t\tcase \"feature_start\": {\n\t\t\trenderer.setPipelineMode(true);\n\t\t\tconst title = event.data?.title as string | undefined;\n\t\t\trenderer.setPhase(title ? `${event.featureId}: ${title}` : event.featureId);\n\n\t\t\tconst totalSteps = event.data?.totalSteps as number | undefined;\n\t\t\tif (totalSteps) {\n\t\t\t\tconst steps: StepDisplay[] = [];\n\t\t\t\tfor (let i = 0; i < totalSteps; i++) {\n\t\t\t\t\tsteps.push({ label: `Step ${i + 1}`, status: \"pending\" });\n\t\t\t\t}\n\t\t\t\trenderer.setSteps(steps);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"step_start\": {\n\t\t\tconst stepIndex = event.data?.stepIndex as number | undefined;\n\t\t\tif (stepIndex !== undefined) {\n\t\t\t\trenderer.updateStep(stepIndex, \"running\");\n\t\t\t}\n\t\t\tif (event.step) {\n\t\t\t\trenderer.updateStepLabel(event.data?.stepIndex as number | undefined, event.step.agent);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"step_complete\": {\n\t\t\tconst stepIndex = event.data?.stepIndex as number | undefined;\n\t\t\tif (stepIndex !== undefined) {\n\t\t\t\tconst elapsed = event.data?.elapsed as number | undefined;\n\t\t\t\tconst usage = event.data?.usage as { cost?: number } | undefined;\n\t\t\t\trenderer.updateStep(stepIndex, \"done\", elapsed !== undefined ? elapsed / 1000 : undefined, usage?.cost);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"step_failed\": {\n\t\t\tconst stepIndex = event.data?.stepIndex as number | undefined;\n\t\t\tif (stepIndex !== undefined) {\n\t\t\t\trenderer.updateStep(stepIndex, \"failed\");\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"pipeline_complete\":\n\t\tcase \"feature_complete\":\n\t\tcase \"orchestration_complete\":\n\t\tcase \"feature_aborted\": {\n\t\t\trenderer.setPipelineMode(false);\n\t\t\trenderer.finalize().catch(() => {});\n\t\t\tbreak;\n\t\t}\n\t}\n}\n"]}
@@ -0,0 +1,380 @@
1
+ /**
2
+ * Telegram bridge — mirrors TUI state to Telegram and accepts simple commands.
3
+ * Activated automatically when token is available (env var or .vibe/config.json).
4
+ * Chat ID is auto-discovered from the first incoming message if not configured.
5
+ */
6
+ import { readFileSync, writeFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { formatToolLabel } from "./formatter.js";
9
+ import { ResponseRenderer } from "./renderer.js";
10
+ import { TelegramAPI } from "./telegram-api.js";
11
+ /**
12
+ * Resolve Telegram config from env vars and .vibe/config.json.
13
+ * Priority: env var > config file.
14
+ */
15
+ export function resolveConfig(vibeDir) {
16
+ let fileToken;
17
+ let fileChatId;
18
+ try {
19
+ const configPath = join(vibeDir, "config.json");
20
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
21
+ const tg = raw.telegram;
22
+ if (tg) {
23
+ fileToken = typeof tg.token === "string" ? tg.token : undefined;
24
+ fileChatId = typeof tg.chatId === "string" ? tg.chatId : undefined;
25
+ }
26
+ }
27
+ catch {
28
+ // Config file may not exist
29
+ }
30
+ const token = process.env.VIBE_TELEGRAM_TOKEN ?? fileToken;
31
+ if (!token)
32
+ return undefined;
33
+ const chatId = process.env.VIBE_TELEGRAM_CHAT_ID ?? fileChatId;
34
+ return { token, chatId: chatId || undefined };
35
+ }
36
+ /** Save discovered chat ID to .vibe/config.json. */
37
+ function saveDiscoveredChatId(vibeDir, chatId) {
38
+ const configPath = join(vibeDir, "config.json");
39
+ let config = {};
40
+ try {
41
+ config = JSON.parse(readFileSync(configPath, "utf-8"));
42
+ }
43
+ catch {
44
+ // File may not exist
45
+ }
46
+ if (!config.telegram || typeof config.telegram !== "object") {
47
+ config.telegram = {};
48
+ }
49
+ config.telegram.chatId = chatId;
50
+ writeFileSync(configPath, `${JSON.stringify(config, null, "\t")}\n`, "utf-8");
51
+ }
52
+ /**
53
+ * Create a bridge manager that supports dynamic mid-session activation.
54
+ * Call `tryStart()` on session_start and agent_end. Call `stop()` on session_shutdown.
55
+ *
56
+ * @param commands Shared array of commands. Items can be added after creation;
57
+ * the polling loop reads from this array by reference on every message.
58
+ */
59
+ export function createTelegramBridgeManager(pi, ctx, vibeDir, commands = []) {
60
+ let cleanup;
61
+ let started = false;
62
+ function tryStart() {
63
+ if (started)
64
+ return;
65
+ const config = resolveConfig(vibeDir);
66
+ if (!config)
67
+ return;
68
+ cleanup = startBridge(pi, ctx, config, vibeDir, commands);
69
+ started = true;
70
+ }
71
+ function stop() {
72
+ cleanup?.();
73
+ cleanup = undefined;
74
+ started = false;
75
+ }
76
+ return { tryStart, stop };
77
+ }
78
+ // ─── Bridge core ─────────────────────────────────────────────────────────────
79
+ function startBridge(pi, ctx, config, vibeDir, commands) {
80
+ const tg = new TelegramAPI(config.token, config.chatId);
81
+ const renderer = new ResponseRenderer(tg);
82
+ const abortController = new AbortController();
83
+ const unsubs = [];
84
+ // Pipeline/gate state tracking — prevents Telegram input during vibe workflows
85
+ // and preserves renderer state between pipeline steps.
86
+ let pipelineActive = false;
87
+ let gateWaiting = false;
88
+ // 1. Agent event mirroring
89
+ pi.on("message_update", (event) => {
90
+ if (event.assistantMessageEvent.type === "text_delta") {
91
+ renderer.appendText(event.assistantMessageEvent.delta);
92
+ }
93
+ });
94
+ pi.on("tool_execution_start", (event) => {
95
+ const label = formatToolLabel(event.toolName, event.args);
96
+ renderer.appendToolInfo(`\u2192 ${label}`);
97
+ });
98
+ pi.on("agent_end", () => {
99
+ // During pipeline execution, don't finalize — the pipeline events manage renderer lifecycle.
100
+ // Finalizing here would reset the step table between steps.
101
+ if (!pipelineActive) {
102
+ renderer.finalize().catch(() => { });
103
+ }
104
+ });
105
+ // 1b. Vibe custom messages (agent responses, milestones, errors, etc.)
106
+ // pi.sendMessage() → sendCustomMessage() only fires _emit() (TUI listeners),
107
+ // not _emitExtensionEvent(), so pi.on("message_end") never fires.
108
+ // Instead, sendVibeMessage() emits vibe:message on the EventBus.
109
+ unsubs.push(pi.events.on("vibe:message", (raw) => {
110
+ const event = raw;
111
+ // Skip categories already handled by pipeline/gate event handlers
112
+ if (event.category === "gate")
113
+ return;
114
+ if (!event.content)
115
+ return;
116
+ const icons = {
117
+ milestone: "\u2728",
118
+ error: "\u274C",
119
+ warning: "\u26A0\uFE0F",
120
+ command: "\u25B6",
121
+ event: "\u{1F4AC}",
122
+ "agent-response": "\u{1F916}",
123
+ "agent-text": "\u270F\uFE0F",
124
+ };
125
+ const icon = icons[event.category] ?? "\u{1F4AC}";
126
+ // For agent-text, show the first line of fullText instead of the generic
127
+ // "[role] ✎ (N lines)" summary. This gives meaningful context (e.g., file
128
+ // being worked on) instead of identical-looking messages.
129
+ let displayText = event.content;
130
+ if (event.category === "agent-text" && event.fullText) {
131
+ const firstLine = event.fullText.split("\n")[0].trim();
132
+ if (firstLine) {
133
+ displayText = firstLine;
134
+ }
135
+ }
136
+ // Send as plain text to avoid Markdown parse failures from LLM output
137
+ tg.sendMessage(`${icon} ${displayText}`, { parse_mode: null }).catch(() => { });
138
+ }));
139
+ // 2. Vibe event mirroring
140
+ unsubs.push(pi.events.on("vibe:pipeline", (raw) => {
141
+ const event = raw;
142
+ if (event.type === "feature_start") {
143
+ pipelineActive = true;
144
+ }
145
+ else if (event.type === "pipeline_complete" ||
146
+ event.type === "feature_complete" ||
147
+ event.type === "orchestration_complete" ||
148
+ event.type === "feature_aborted") {
149
+ pipelineActive = false;
150
+ gateWaiting = false;
151
+ }
152
+ handlePipelineEvent(event, renderer);
153
+ }), pi.events.on("vibe:activity", (raw) => {
154
+ const event = raw;
155
+ renderer.addActivity(event.text);
156
+ }), pi.events.on("vibe:gate", (raw) => {
157
+ const event = raw;
158
+ gateWaiting = true;
159
+ const keyboard = {
160
+ inline_keyboard: [
161
+ [
162
+ { text: "\u2705 Approve", callback_data: "Approve" },
163
+ { text: "\u274C Reject", callback_data: "Reject" },
164
+ ],
165
+ [
166
+ { text: "\u23ED Skip", callback_data: "Skip" },
167
+ { text: "\u26D4 Abort", callback_data: "Abort" },
168
+ ],
169
+ ],
170
+ };
171
+ tg.sendMessage(`\u23F3 Gate: ${event.summary}`, { reply_markup: keyboard }).catch(() => { });
172
+ }));
173
+ // Reset gate flag when pipeline resumes after gate approval
174
+ unsubs.push(pi.events.on("vibe:pipeline", (raw) => {
175
+ const event = raw;
176
+ if (event.type === "step_start") {
177
+ gateWaiting = false;
178
+ }
179
+ }));
180
+ // 3. Polling loop (fire-and-forget)
181
+ startPolling(tg, pi, ctx, abortController.signal, vibeDir, {
182
+ isVibeBusy: () => pipelineActive || gateWaiting,
183
+ isGateWaiting: () => gateWaiting,
184
+ clearGateWaiting: () => {
185
+ gateWaiting = false;
186
+ },
187
+ commands,
188
+ });
189
+ // 4. Register bot menu commands (including subcommands as separate entries)
190
+ const menuCommands = [
191
+ { command: "stop", description: "Stop current execution" },
192
+ ];
193
+ for (const cmd of commands) {
194
+ menuCommands.push({ command: cmd.name, description: cmd.description ?? `/${cmd.name}` });
195
+ if (cmd.subcommands) {
196
+ for (const sub of cmd.subcommands) {
197
+ // Telegram command names: lowercase, digits, underscores only
198
+ menuCommands.push({ command: `${cmd.name}_${sub.name}`, description: sub.description });
199
+ }
200
+ }
201
+ }
202
+ tg.setMyCommands(menuCommands).catch(() => { });
203
+ // 5. Startup
204
+ if (tg.hasChatId()) {
205
+ const model = ctx.model;
206
+ const startupMsg = model
207
+ ? `Telegram bridge active. Model: ${model.provider}/${model.id}`
208
+ : "Telegram bridge active. No model.";
209
+ tg.sendMessage(startupMsg).catch(() => { });
210
+ }
211
+ else {
212
+ ctx.ui.notify("[Telegram] Waiting for first message from your bot", "info");
213
+ }
214
+ // Return cleanup function
215
+ return () => {
216
+ abortController.abort();
217
+ for (const unsub of unsubs)
218
+ unsub();
219
+ };
220
+ }
221
+ // ─── Polling ─────────────────────────────────────────────────────────────────
222
+ function sleep(ms) {
223
+ return new Promise((resolve) => setTimeout(resolve, ms));
224
+ }
225
+ async function startPolling(tg, pi, ctx, signal, vibeDir, state) {
226
+ while (!signal.aborted) {
227
+ try {
228
+ const updates = await tg.getUpdates();
229
+ for (const update of updates) {
230
+ if (signal.aborted)
231
+ break;
232
+ // Handle callback queries (gate button clicks)
233
+ if (update.callback_query) {
234
+ const data = update.callback_query.data ?? "";
235
+ tg.answerCallbackQuery(update.callback_query.id).catch(() => { });
236
+ if (state.isGateWaiting()) {
237
+ pi.events.emit("vibe:gate_response", { choice: data });
238
+ state.clearGateWaiting();
239
+ }
240
+ continue;
241
+ }
242
+ // Auto-discover chat ID from first incoming message
243
+ if (!tg.hasChatId() && update.message?.chat.id) {
244
+ const discoveredId = String(update.message.chat.id);
245
+ tg.setChatId(discoveredId);
246
+ saveDiscoveredChatId(vibeDir, discoveredId);
247
+ const model = ctx.model;
248
+ const msg = model
249
+ ? `Telegram bridge connected. Model: ${model.provider}/${model.id}`
250
+ : "Telegram bridge connected.";
251
+ await tg.sendMessage(msg).catch(() => { });
252
+ ctx.ui.notify("[Telegram] Connected", "info");
253
+ continue; // Skip processing discovery message as a command
254
+ }
255
+ const text = update.message?.text;
256
+ if (!text)
257
+ continue;
258
+ if (text === "/stop") {
259
+ ctx.abort();
260
+ await tg.sendMessage("\u23F8 Stopped.").catch(() => { });
261
+ continue;
262
+ }
263
+ // Block input during pipeline execution or gate waiting
264
+ if (state.isVibeBusy()) {
265
+ await tg.sendMessage("\u23F3 Pipeline running. Wait or /stop.").catch(() => { });
266
+ continue;
267
+ }
268
+ if (ctx.isIdle()) {
269
+ // Try to dispatch as extension command first.
270
+ // Fire-and-forget: command handlers may block on gates/UI,
271
+ // and the polling loop must keep running to receive callback queries.
272
+ if (text.startsWith("/") && tryDispatchCommandSync(text, state.commands, ctx)) {
273
+ continue;
274
+ }
275
+ pi.sendUserMessage(text);
276
+ }
277
+ else {
278
+ await tg.sendMessage("\u23F3 Busy. Wait or /stop.").catch(() => { });
279
+ }
280
+ }
281
+ }
282
+ catch {
283
+ if (signal.aborted)
284
+ break;
285
+ await sleep(5000);
286
+ }
287
+ }
288
+ }
289
+ // ─── Command dispatch ────────────────────────────────────────────────────────
290
+ /**
291
+ * Try to dispatch a slash command to a registered handler.
292
+ * Handles both direct commands (/vibe resume) and Telegram-style
293
+ * subcommands (/vibe_resume) by mapping underscores back to spaces.
294
+ *
295
+ * Returns true if the command was matched. The handler runs as
296
+ * fire-and-forget so the polling loop is never blocked (handlers
297
+ * may await gates/UI dialogs indefinitely).
298
+ */
299
+ function tryDispatchCommandSync(text, commands, ctx) {
300
+ // Strip leading / and optional @BotName suffix (Telegram adds this in groups)
301
+ const raw = text.slice(1).split("@")[0];
302
+ const spaceIndex = raw.indexOf(" ");
303
+ const fullName = spaceIndex === -1 ? raw : raw.slice(0, spaceIndex);
304
+ const trailingArgs = spaceIndex === -1 ? "" : raw.slice(spaceIndex + 1);
305
+ // 1. Direct match: /vibe resume → command="vibe", args="resume"
306
+ const directMatch = commands.find((c) => c.name === fullName);
307
+ if (directMatch) {
308
+ directMatch.handler(trailingArgs, ctx).catch(() => { });
309
+ return true;
310
+ }
311
+ // 2. Subcommand match: /vibe_resume → command="vibe", args="resume"
312
+ for (const cmd of commands) {
313
+ if (!cmd.subcommands)
314
+ continue;
315
+ const prefix = `${cmd.name}_`;
316
+ if (fullName.startsWith(prefix)) {
317
+ const subName = fullName.slice(prefix.length);
318
+ if (cmd.subcommands.some((s) => s.name === subName)) {
319
+ const combinedArgs = trailingArgs ? `${subName} ${trailingArgs}` : subName;
320
+ cmd.handler(combinedArgs, ctx).catch(() => { });
321
+ return true;
322
+ }
323
+ }
324
+ }
325
+ return false;
326
+ }
327
+ // ─── Pipeline event handler ─────────────────────────────────────────────────
328
+ function handlePipelineEvent(event, renderer) {
329
+ switch (event.type) {
330
+ case "feature_start": {
331
+ renderer.setPipelineMode(true);
332
+ const title = event.data?.title;
333
+ renderer.setPhase(title ? `${event.featureId}: ${title}` : event.featureId);
334
+ const totalSteps = event.data?.totalSteps;
335
+ if (totalSteps) {
336
+ const steps = [];
337
+ for (let i = 0; i < totalSteps; i++) {
338
+ steps.push({ label: `Step ${i + 1}`, status: "pending" });
339
+ }
340
+ renderer.setSteps(steps);
341
+ }
342
+ break;
343
+ }
344
+ case "step_start": {
345
+ const stepIndex = event.data?.stepIndex;
346
+ if (stepIndex !== undefined) {
347
+ renderer.updateStep(stepIndex, "running");
348
+ }
349
+ if (event.step) {
350
+ renderer.updateStepLabel(event.data?.stepIndex, event.step.agent);
351
+ }
352
+ break;
353
+ }
354
+ case "step_complete": {
355
+ const stepIndex = event.data?.stepIndex;
356
+ if (stepIndex !== undefined) {
357
+ const elapsed = event.data?.elapsed;
358
+ const usage = event.data?.usage;
359
+ renderer.updateStep(stepIndex, "done", elapsed !== undefined ? elapsed / 1000 : undefined, usage?.cost);
360
+ }
361
+ break;
362
+ }
363
+ case "step_failed": {
364
+ const stepIndex = event.data?.stepIndex;
365
+ if (stepIndex !== undefined) {
366
+ renderer.updateStep(stepIndex, "failed");
367
+ }
368
+ break;
369
+ }
370
+ case "pipeline_complete":
371
+ case "feature_complete":
372
+ case "orchestration_complete":
373
+ case "feature_aborted": {
374
+ renderer.setPipelineMode(false);
375
+ renderer.finalize().catch(() => { });
376
+ break;
377
+ }
378
+ }
379
+ }
380
+ //# sourceMappingURL=bridge.js.map