agentic-loop 3.13.0 → 3.14.2

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 (57) hide show
  1. package/.claude/skills/idea/SKILL.md +56 -0
  2. package/.claude/skills/loopgram/SKILL.md +19 -0
  3. package/.claude/skills/prd/SKILL.md +2 -0
  4. package/README.md +1 -0
  5. package/bin/ralph.sh +17 -11
  6. package/dist/loopgram/claude.d.ts +18 -0
  7. package/dist/loopgram/claude.d.ts.map +1 -0
  8. package/dist/loopgram/claude.js +89 -0
  9. package/dist/loopgram/claude.js.map +1 -0
  10. package/dist/loopgram/context-search.d.ts +26 -0
  11. package/dist/loopgram/context-search.d.ts.map +1 -0
  12. package/dist/loopgram/context-search.js +175 -0
  13. package/dist/loopgram/context-search.js.map +1 -0
  14. package/dist/loopgram/conversation.d.ts +39 -0
  15. package/dist/loopgram/conversation.d.ts.map +1 -0
  16. package/dist/loopgram/conversation.js +158 -0
  17. package/dist/loopgram/conversation.js.map +1 -0
  18. package/dist/loopgram/index.d.ts +3 -0
  19. package/dist/loopgram/index.d.ts.map +1 -0
  20. package/dist/loopgram/index.js +246 -0
  21. package/dist/loopgram/index.js.map +1 -0
  22. package/dist/loopgram/loop-monitor.d.ts +16 -0
  23. package/dist/loopgram/loop-monitor.d.ts.map +1 -0
  24. package/dist/loopgram/loop-monitor.js +149 -0
  25. package/dist/loopgram/loop-monitor.js.map +1 -0
  26. package/dist/loopgram/loop-runner.d.ts +28 -0
  27. package/dist/loopgram/loop-runner.d.ts.map +1 -0
  28. package/dist/loopgram/loop-runner.js +157 -0
  29. package/dist/loopgram/loop-runner.js.map +1 -0
  30. package/dist/loopgram/prd-generator.d.ts +37 -0
  31. package/dist/loopgram/prd-generator.d.ts.map +1 -0
  32. package/dist/loopgram/prd-generator.js +134 -0
  33. package/dist/loopgram/prd-generator.js.map +1 -0
  34. package/dist/loopgram/saver.d.ts +9 -0
  35. package/dist/loopgram/saver.d.ts.map +1 -0
  36. package/dist/loopgram/saver.js +35 -0
  37. package/dist/loopgram/saver.js.map +1 -0
  38. package/dist/loopgram/types.d.ts +37 -0
  39. package/dist/loopgram/types.d.ts.map +1 -0
  40. package/dist/loopgram/types.js +5 -0
  41. package/dist/loopgram/types.js.map +1 -0
  42. package/package.json +6 -2
  43. package/ralph/hooks/common.sh +89 -0
  44. package/ralph/hooks/warn-debug.sh +14 -32
  45. package/ralph/hooks/warn-empty-catch.sh +13 -29
  46. package/ralph/hooks/warn-secrets.sh +19 -37
  47. package/ralph/hooks/warn-urls.sh +17 -33
  48. package/ralph/loop.sh +5 -2
  49. package/ralph/prd-check.sh +35 -8
  50. package/ralph/setup/quick-setup.sh +25 -12
  51. package/ralph/setup/ui.sh +0 -42
  52. package/ralph/setup.sh +71 -46
  53. package/ralph/utils.sh +167 -31
  54. package/templates/config/fastmcp.json +6 -1
  55. package/templates/config/fullstack.json +8 -0
  56. package/templates/config/node.json +8 -0
  57. package/templates/config/python.json +8 -0
@@ -0,0 +1,157 @@
1
+ import { spawn } from 'child_process';
2
+ import { existsSync, writeFileSync, readFileSync, unlinkSync } from 'fs';
3
+ import { join } from 'path';
4
+ const PID_FILE = '.ralph/loop.pid';
5
+ /**
6
+ * Check if a Ralph loop is currently running for a project
7
+ */
8
+ export function isLoopRunning(projectPath) {
9
+ const pidFile = join(projectPath, PID_FILE);
10
+ if (!existsSync(pidFile)) {
11
+ return false;
12
+ }
13
+ try {
14
+ const pid = parseInt(readFileSync(pidFile, 'utf-8').trim());
15
+ // Check if process is still running
16
+ process.kill(pid, 0);
17
+ return true;
18
+ }
19
+ catch {
20
+ // Process not running, clean up stale PID file
21
+ try {
22
+ unlinkSync(pidFile);
23
+ }
24
+ catch {
25
+ // Ignore cleanup errors
26
+ }
27
+ return false;
28
+ }
29
+ }
30
+ /**
31
+ * Start a Ralph loop for a project
32
+ */
33
+ export function startLoop(projectPath) {
34
+ // Check if PRD exists
35
+ const prdPath = join(projectPath, '.ralph/prd.json');
36
+ if (!existsSync(prdPath)) {
37
+ return {
38
+ success: false,
39
+ message: 'No PRD found. Use /prd to create stories first.',
40
+ };
41
+ }
42
+ // Check if already running
43
+ if (isLoopRunning(projectPath)) {
44
+ return {
45
+ success: false,
46
+ message: 'Loop already running. Use /loop to check status.',
47
+ };
48
+ }
49
+ try {
50
+ // Start Ralph loop in background
51
+ const logFile = join(projectPath, '.ralph/loop.log');
52
+ // Use npx agentic-loop run
53
+ const child = spawn('npx', ['agentic-loop', 'run'], {
54
+ cwd: projectPath,
55
+ detached: true,
56
+ stdio: ['ignore', 'pipe', 'pipe'],
57
+ env: {
58
+ ...process.env,
59
+ // Ensure Claude runs non-interactively
60
+ CLAUDE_CODE_ENTRYPOINT: 'cli',
61
+ },
62
+ });
63
+ // Write output to log file
64
+ const logStream = require('fs').createWriteStream(logFile, { flags: 'a' });
65
+ child.stdout?.pipe(logStream);
66
+ child.stderr?.pipe(logStream);
67
+ // Save PID
68
+ const pidFile = join(projectPath, PID_FILE);
69
+ writeFileSync(pidFile, child.pid.toString());
70
+ // Detach child process
71
+ child.unref();
72
+ return {
73
+ success: true,
74
+ message: `Loop started (PID: ${child.pid}). Use /loop to check progress.`,
75
+ pid: child.pid,
76
+ };
77
+ }
78
+ catch (error) {
79
+ return {
80
+ success: false,
81
+ message: `Failed to start loop: ${error}`,
82
+ };
83
+ }
84
+ }
85
+ /**
86
+ * Stop a running Ralph loop
87
+ */
88
+ export function stopLoop(projectPath) {
89
+ const pidFile = join(projectPath, PID_FILE);
90
+ if (!existsSync(pidFile)) {
91
+ return {
92
+ success: false,
93
+ message: 'No loop running.',
94
+ };
95
+ }
96
+ try {
97
+ const pid = parseInt(readFileSync(pidFile, 'utf-8').trim());
98
+ // Kill the process and its children
99
+ try {
100
+ // Kill process group (negative PID)
101
+ process.kill(-pid, 'SIGTERM');
102
+ }
103
+ catch {
104
+ // Try killing just the process
105
+ process.kill(pid, 'SIGTERM');
106
+ }
107
+ // Clean up PID file
108
+ unlinkSync(pidFile);
109
+ return {
110
+ success: true,
111
+ message: 'Loop stopped.',
112
+ };
113
+ }
114
+ catch (error) {
115
+ // Clean up PID file even if kill failed
116
+ try {
117
+ unlinkSync(pidFile);
118
+ }
119
+ catch {
120
+ // Ignore
121
+ }
122
+ return {
123
+ success: false,
124
+ message: `Error stopping loop: ${error}`,
125
+ };
126
+ }
127
+ }
128
+ /**
129
+ * Get loop process info
130
+ */
131
+ export function getLoopInfo(projectPath) {
132
+ const pidFile = join(projectPath, PID_FILE);
133
+ const logFile = join(projectPath, '.ralph/loop.log');
134
+ const running = isLoopRunning(projectPath);
135
+ let pid;
136
+ let logTail;
137
+ if (existsSync(pidFile)) {
138
+ try {
139
+ pid = parseInt(readFileSync(pidFile, 'utf-8').trim());
140
+ }
141
+ catch {
142
+ // Ignore
143
+ }
144
+ }
145
+ if (existsSync(logFile)) {
146
+ try {
147
+ // Get last 500 chars of log
148
+ const content = readFileSync(logFile, 'utf-8');
149
+ logTail = content.slice(-500);
150
+ }
151
+ catch {
152
+ // Ignore
153
+ }
154
+ }
155
+ return { running, pid, logTail };
156
+ }
157
+ //# sourceMappingURL=loop-runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loop-runner.js","sourceRoot":"","sources":["../../src/loopgram/loop-runner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import type { Message } from './types.js';
2
+ interface Story {
3
+ id: string;
4
+ title: string;
5
+ description: string;
6
+ acceptanceCriteria: string[];
7
+ testSteps: string[];
8
+ status: 'pending' | 'in_progress' | 'completed' | 'failed';
9
+ }
10
+ /**
11
+ * Generate PRD stories from a conversation
12
+ */
13
+ export declare function generateStories(conversation: Message[], context: string | undefined, model: string): Promise<{
14
+ featureName: string;
15
+ featureDescription: string;
16
+ stories: Story[];
17
+ }>;
18
+ /**
19
+ * Append stories to existing PRD or create new one
20
+ */
21
+ export declare function appendToPRD(projectPath: string, featureName: string, featureDescription: string, newStories: Story[]): {
22
+ added: number;
23
+ total: number;
24
+ prdPath: string;
25
+ };
26
+ /**
27
+ * Get current PRD status
28
+ */
29
+ export declare function getPRDStatus(projectPath: string): {
30
+ exists: boolean;
31
+ featureName?: string;
32
+ total: number;
33
+ pending: number;
34
+ completed: number;
35
+ } | null;
36
+ export {};
37
+ //# sourceMappingURL=prd-generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prd-generator.d.ts","sourceRoot":"","sources":["../../src/loopgram/prd-generator.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAI1C,UAAU,KAAK;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,SAAS,GAAG,aAAa,GAAG,WAAW,GAAG,QAAQ,CAAC;CAC5D;AAmCD;;GAEG;AACH,wBAAsB,eAAe,CACnC,YAAY,EAAE,OAAO,EAAE,EACvB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,KAAK,EAAE,CAAA;CAAE,CAAC,CAwChF;AAED;;GAEG;AACH,wBAAgB,WAAW,CACzB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,kBAAkB,EAAE,MAAM,EAC1B,UAAU,EAAE,KAAK,EAAE,GAClB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CA4CnD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG;IACjD,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,IAAI,CAwBP"}
@@ -0,0 +1,134 @@
1
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import Anthropic from '@anthropic-ai/sdk';
4
+ const anthropic = new Anthropic();
5
+ const PRD_PROMPT = `You are generating stories for a PRD (Product Requirements Document) that will be executed by an autonomous coding agent called Ralph.
6
+
7
+ Based on the conversation, generate 1-3 small, focused stories. Each story should be completable in a single coding session.
8
+
9
+ Output ONLY valid JSON in this exact format:
10
+ {
11
+ "featureName": "Brief feature name",
12
+ "featureDescription": "One sentence description",
13
+ "stories": [
14
+ {
15
+ "id": "kebab-case-id",
16
+ "title": "Short title",
17
+ "description": "What needs to be built",
18
+ "acceptanceCriteria": ["Criterion 1", "Criterion 2"],
19
+ "testSteps": ["npm test", "npm run lint"]
20
+ }
21
+ ]
22
+ }
23
+
24
+ Rules:
25
+ - Story IDs must be unique kebab-case (e.g., "add-oauth-config")
26
+ - Keep stories small and focused
27
+ - Include realistic test commands in testSteps
28
+ - Output ONLY the JSON, no markdown or explanation`;
29
+ /**
30
+ * Generate PRD stories from a conversation
31
+ */
32
+ export async function generateStories(conversation, context, model) {
33
+ let prompt = 'Generate PRD stories from this conversation:\n\n';
34
+ if (context) {
35
+ prompt += `## Codebase Context\n${context}\n\n`;
36
+ }
37
+ prompt += '## Conversation\n';
38
+ for (const msg of conversation) {
39
+ prompt += `${msg.role.toUpperCase()}: ${msg.content}\n\n`;
40
+ }
41
+ const response = await anthropic.messages.create({
42
+ model,
43
+ max_tokens: 2000,
44
+ system: PRD_PROMPT,
45
+ messages: [{ role: 'user', content: prompt }],
46
+ });
47
+ const text = response.content[0];
48
+ if (text.type !== 'text') {
49
+ throw new Error('Unexpected response type');
50
+ }
51
+ // Parse JSON from response (handle markdown code blocks)
52
+ let jsonStr = text.text.trim();
53
+ if (jsonStr.startsWith('```')) {
54
+ jsonStr = jsonStr.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '');
55
+ }
56
+ const result = JSON.parse(jsonStr);
57
+ return {
58
+ featureName: result.featureName,
59
+ featureDescription: result.featureDescription,
60
+ stories: result.stories.map((s) => ({
61
+ ...s,
62
+ status: 'pending',
63
+ })),
64
+ };
65
+ }
66
+ /**
67
+ * Append stories to existing PRD or create new one
68
+ */
69
+ export function appendToPRD(projectPath, featureName, featureDescription, newStories) {
70
+ const prdPath = join(projectPath, '.ralph/prd.json');
71
+ let prd;
72
+ if (existsSync(prdPath)) {
73
+ // Load existing PRD
74
+ const content = readFileSync(prdPath, 'utf-8');
75
+ prd = JSON.parse(content);
76
+ // Check for duplicate story IDs and make unique
77
+ const existingIds = new Set(prd.stories.map((s) => s.id));
78
+ for (const story of newStories) {
79
+ let id = story.id;
80
+ let counter = 1;
81
+ while (existingIds.has(id)) {
82
+ id = `${story.id}-${counter}`;
83
+ counter++;
84
+ }
85
+ story.id = id;
86
+ existingIds.add(id);
87
+ }
88
+ // Append new stories
89
+ prd.stories.push(...newStories);
90
+ }
91
+ else {
92
+ // Create new PRD
93
+ prd = {
94
+ feature: {
95
+ name: featureName,
96
+ description: featureDescription,
97
+ },
98
+ stories: newStories,
99
+ };
100
+ }
101
+ // Write PRD
102
+ writeFileSync(prdPath, JSON.stringify(prd, null, 2));
103
+ return {
104
+ added: newStories.length,
105
+ total: prd.stories.length,
106
+ prdPath,
107
+ };
108
+ }
109
+ /**
110
+ * Get current PRD status
111
+ */
112
+ export function getPRDStatus(projectPath) {
113
+ const prdPath = join(projectPath, '.ralph/prd.json');
114
+ if (!existsSync(prdPath)) {
115
+ return { exists: false, total: 0, pending: 0, completed: 0 };
116
+ }
117
+ try {
118
+ const content = readFileSync(prdPath, 'utf-8');
119
+ const prd = JSON.parse(content);
120
+ const pending = prd.stories.filter((s) => s.status === 'pending').length;
121
+ const completed = prd.stories.filter((s) => s.status === 'completed').length;
122
+ return {
123
+ exists: true,
124
+ featureName: prd.feature.name,
125
+ total: prd.stories.length,
126
+ pending,
127
+ completed,
128
+ };
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ }
134
+ //# sourceMappingURL=prd-generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prd-generator.js","sourceRoot":"","sources":["../../src/loopgram/prd-generator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Save a brainstorming idea to docs/ideas/ in the specified project
3
+ */
4
+ export declare function saveIdea(projectPath: string, filename: string, content: string): string;
5
+ /**
6
+ * Generate a filename-safe slug from a title
7
+ */
8
+ export declare function slugify(title: string): string;
9
+ //# sourceMappingURL=saver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"saver.d.ts","sourceRoot":"","sources":["../../src/loopgram/saver.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,wBAAgB,QAAQ,CACtB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,MAAM,CAsBR;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM7C"}
@@ -0,0 +1,35 @@
1
+ import { writeFileSync, mkdirSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ /**
4
+ * Save a brainstorming idea to docs/ideas/ in the specified project
5
+ */
6
+ export function saveIdea(projectPath, filename, content) {
7
+ const ideasDir = join(projectPath, 'docs/ideas');
8
+ // Ensure directory exists
9
+ if (!existsSync(ideasDir)) {
10
+ mkdirSync(ideasDir, { recursive: true });
11
+ }
12
+ const filepath = join(ideasDir, `${filename}.md`);
13
+ // Add metadata header
14
+ const fullContent = `---
15
+ created: ${new Date().toISOString()}
16
+ source: telegram-brainstorm
17
+ status: idea
18
+ ---
19
+
20
+ ${content}
21
+ `;
22
+ writeFileSync(filepath, fullContent);
23
+ return filepath;
24
+ }
25
+ /**
26
+ * Generate a filename-safe slug from a title
27
+ */
28
+ export function slugify(title) {
29
+ return title
30
+ .toLowerCase()
31
+ .replace(/[^a-z0-9]+/g, '-')
32
+ .replace(/^-|-$/g, '')
33
+ .slice(0, 50);
34
+ }
35
+ //# sourceMappingURL=saver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"saver.js","sourceRoot":"","sources":["../../src/loopgram/saver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAC1D,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B;;GAEG;AACH,MAAM,UAAU,QAAQ,CACtB,WAAmB,EACnB,QAAgB,EAChB,OAAe;IAEf,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;IAEjD,0BAA0B;IAC1B,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,GAAG,QAAQ,KAAK,CAAC,CAAC;IAElD,sBAAsB;IACtB,MAAM,WAAW,GAAG;WACX,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;;;;;EAKjC,OAAO;CACR,CAAC;IAEA,aAAa,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IACrC,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,OAAO,CAAC,KAAa;IACnC,OAAO,KAAK;SACT,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;SACrB,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAClB,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * TypeScript interfaces for the Telegram Brainstorm Bot
3
+ */
4
+ export interface Message {
5
+ role: 'user' | 'assistant';
6
+ content: string;
7
+ }
8
+ export interface ProjectConfig {
9
+ name: string;
10
+ path: string;
11
+ description?: string;
12
+ }
13
+ export interface TelegramConfig {
14
+ allowedUserIds: string[];
15
+ }
16
+ export interface AnthropicConfig {
17
+ model: string;
18
+ }
19
+ export interface BrainstormConfig {
20
+ telegram: TelegramConfig;
21
+ anthropic: AnthropicConfig;
22
+ projects: Record<string, ProjectConfig>;
23
+ }
24
+ export interface SaveResult {
25
+ filepath: string;
26
+ summary: string;
27
+ title: string;
28
+ }
29
+ export interface LoopStatus {
30
+ isRunning: boolean;
31
+ currentStory: string | null;
32
+ completedStories: number;
33
+ totalStories: number;
34
+ lastUpdate: string;
35
+ errors: string[];
36
+ }
37
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/loopgram/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,cAAc,CAAC;IACzB,SAAS,EAAE,eAAe,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * TypeScript interfaces for the Telegram Brainstorm Bot
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/loopgram/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-loop",
3
- "version": "3.13.0",
3
+ "version": "3.14.2",
4
4
  "description": "Autonomous AI coding loop - PRD-driven development with Claude Code",
5
5
  "author": "Allie Jones <allie@allthrive.ai>",
6
6
  "license": "MIT",
@@ -58,7 +58,9 @@
58
58
  "postinstall": "./bin/postinstall.sh",
59
59
  "release": "npm version patch && git push && git push --tags && npm publish",
60
60
  "release:minor": "npm version minor && git push && git push --tags && npm publish",
61
- "release:major": "npm version major && git push && git push --tags && npm publish"
61
+ "release:major": "npm version major && git push && git push --tags && npm publish",
62
+ "loopgram": "tsx src/loopgram/index.ts",
63
+ "loopgram:bg": "nohup npm run loopgram > .ralph/loopgram.log 2>&1 &"
62
64
  },
63
65
  "engines": {
64
66
  "node": ">=18.0.0"
@@ -72,6 +74,8 @@
72
74
  }
73
75
  },
74
76
  "dependencies": {
77
+ "@anthropic-ai/sdk": "^0.32.0",
78
+ "telegraf": "^4.16.0",
75
79
  "tsx": "^4.0.0"
76
80
  },
77
81
  "devDependencies": {
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env bash
2
+ # shellcheck shell=bash
3
+ # common.sh - Shared utilities for Claude Code hooks
4
+ #
5
+ # Source this file at the start of PostToolUse hooks:
6
+ # source "$(dirname "$0")/common.sh"
7
+ #
8
+ # Provides:
9
+ # - parse_hook_input: Sets HOOK_INPUT, TOOL_NAME, FILE_PATH, NEW_CONTENT
10
+ # - is_code_file: Check if FILE_PATH matches code extensions
11
+ # - is_test_file: Check if FILE_PATH is a test file
12
+ # - hook_allow: Output JSON to allow the operation
13
+ # - hook_warn: Output JSON warning (non-blocking)
14
+ # - hook_block: Output JSON to block the operation
15
+
16
+ set -euo pipefail
17
+
18
+ # Global variables set by parse_hook_input
19
+ HOOK_INPUT=""
20
+ TOOL_NAME=""
21
+ FILE_PATH=""
22
+ NEW_CONTENT=""
23
+
24
+ # Parse hook input from stdin and extract common fields
25
+ # Sets: HOOK_INPUT, TOOL_NAME, FILE_PATH, NEW_CONTENT
26
+ parse_hook_input() {
27
+ HOOK_INPUT=$(cat)
28
+ TOOL_NAME=$(echo "$HOOK_INPUT" | jq -r '.tool_name // ""')
29
+ FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
30
+
31
+ # Extract content based on tool type
32
+ NEW_CONTENT=""
33
+ if [[ "$TOOL_NAME" == "Write" ]]; then
34
+ NEW_CONTENT=$(echo "$HOOK_INPUT" | jq -r '.tool_input.content // ""')
35
+ elif [[ "$TOOL_NAME" == "Edit" ]]; then
36
+ NEW_CONTENT=$(echo "$HOOK_INPUT" | jq -r '.tool_input.new_string // ""')
37
+ fi
38
+ }
39
+
40
+ # Check if FILE_PATH matches given extensions
41
+ # Usage: is_code_file "ts,tsx,js,jsx,py" && echo "yes"
42
+ is_code_file() {
43
+ local extensions="$1"
44
+
45
+ # Files without extension don't match
46
+ [[ "$FILE_PATH" != *"."* ]] && return 1
47
+
48
+ local file_ext="${FILE_PATH##*.}"
49
+
50
+ # Check if file extension is in the comma-separated list
51
+ [[ ",$extensions," == *",$file_ext,"* ]]
52
+ }
53
+
54
+ # Check if FILE_PATH is a test file
55
+ is_test_file() {
56
+ case "$FILE_PATH" in
57
+ *.test.*|*.spec.*|*/__tests__/*|*/test/*|*/tests/*|*/fixtures/*)
58
+ return 0
59
+ ;;
60
+ esac
61
+ return 1
62
+ }
63
+
64
+ # Output JSON to allow the operation (continue: true)
65
+ hook_allow() {
66
+ echo '{"continue": true}'
67
+ }
68
+
69
+ # Output warning JSON (non-blocking, adds context)
70
+ # Usage: hook_warn "Warning message"
71
+ hook_warn() {
72
+ local message="$1"
73
+ jq -n --arg warn "$message" '{
74
+ "continue": true,
75
+ "hookSpecificOutput": {
76
+ "additionalContext": $warn
77
+ }
78
+ }'
79
+ }
80
+
81
+ # Output blocking JSON (continue: false)
82
+ # Usage: hook_block "Error message"
83
+ hook_block() {
84
+ local message="$1"
85
+ jq -n --arg msg "$message" '{
86
+ "continue": false,
87
+ "message": $msg
88
+ }'
89
+ }
@@ -1,54 +1,36 @@
1
1
  #!/usr/bin/env bash
2
+ # shellcheck shell=bash
2
3
  # warn-debug.sh - Warn about debug statements in written code
3
4
  # Hook: PostToolUse matcher: "Edit|Write"
4
5
 
5
- set -euo pipefail
6
+ source "$(dirname "$0")/common.sh"
6
7
 
7
- INPUT=$(cat)
8
- TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
9
- FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
8
+ parse_hook_input
10
9
 
11
10
  # Only check code files
12
- case "$FILE_PATH" in
13
- *.ts|*.tsx|*.js|*.jsx|*.py|*.go|*.rs)
14
- ;;
15
- *)
16
- echo '{"continue": true}'
17
- exit 0
18
- ;;
19
- esac
20
-
21
- # Get the content that was written
22
- NEW_CONTENT=""
23
- if [[ "$TOOL_NAME" == "Write" ]]; then
24
- NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // ""')
25
- elif [[ "$TOOL_NAME" == "Edit" ]]; then
26
- NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // ""')
11
+ if ! is_code_file "ts,tsx,js,jsx,py,go,rs"; then
12
+ hook_allow
13
+ exit 0
27
14
  fi
28
15
 
29
16
  # Check for debug patterns
30
17
  WARNINGS=""
31
18
 
32
- if echo "$NEW_CONTENT" | grep -qE 'console\.(log|debug|info|warn|error)\s*\('; then
19
+ if echo "$NEW_CONTENT" | grep -qE 'console\.(log|debug|info|warn|error)[[:space:]]*\('; then
33
20
  WARNINGS="⚠️ Debug statement detected: console.log/debug. Remove before commit."
34
21
  fi
35
22
 
36
- if echo "$NEW_CONTENT" | grep -qE '^\s*debugger\s*;?\s*$'; then
37
- WARNINGS="${WARNINGS}\n⚠️ Debugger statement detected. Remove before commit."
23
+ if echo "$NEW_CONTENT" | grep -qE '^[[:space:]]*debugger[[:space:]]*;?[[:space:]]*$'; then
24
+ WARNINGS="${WARNINGS}${WARNINGS:+\\n}⚠️ Debugger statement detected. Remove before commit."
38
25
  fi
39
26
 
40
- if echo "$NEW_CONTENT" | grep -qE '^\s*print\s*\('; then
41
- WARNINGS="${WARNINGS}\n⚠️ Print statement detected. Remove before commit."
27
+ if echo "$NEW_CONTENT" | grep -qE '^[[:space:]]*print[[:space:]]*\('; then
28
+ WARNINGS="${WARNINGS}${WARNINGS:+\\n}⚠️ Print statement detected. Remove before commit."
42
29
  fi
43
30
 
44
- # Output warning as additional context (non-blocking)
31
+ # Output warning or allow
45
32
  if [[ -n "$WARNINGS" ]]; then
46
- jq -n --arg warn "$WARNINGS" '{
47
- "continue": true,
48
- "hookSpecificOutput": {
49
- "additionalContext": $warn
50
- }
51
- }'
33
+ hook_warn "$WARNINGS"
52
34
  else
53
- echo '{"continue": true}'
35
+ hook_allow
54
36
  fi