@forloop-cc/forloop-opencode-plugin-planner 2.2.1

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 (55) hide show
  1. package/CHANGELOG.md +225 -0
  2. package/CODE_OF_CONDUCT.md +77 -0
  3. package/CONTRIBUTING.md +176 -0
  4. package/LICENSE +21 -0
  5. package/README.md +211 -0
  6. package/SECURITY.md +69 -0
  7. package/docs/CONFIGURATION.md +283 -0
  8. package/docs/DEVELOPER-VS-USER.md +129 -0
  9. package/docs/IMPROVED-AUTHENTICATION.md +317 -0
  10. package/docs/INSTALLATION-SCRIPTS.md +304 -0
  11. package/docs/PLANNER-AGENT-IMPROVEMENTS.md +88 -0
  12. package/docs/PLUGIN-ARCHITECTURE.md +308 -0
  13. package/docs/QUICKSTART-NON-DEV.md +94 -0
  14. package/docs/README-FOR-HUMANS-AND-AI.md +109 -0
  15. package/docs/README.md +70 -0
  16. package/docs/SKILLS-GAP-ANALYSIS.md +157 -0
  17. package/docs/USER-INTERACTION-MODEL.md +106 -0
  18. package/docs/file-management/2026-04-12-forloop-aivy-documents-sync.md +395 -0
  19. package/docs/file-management/FILE_UPLOAD_ENHANCEMENT.md +431 -0
  20. package/docs/file-management/FILE_UPLOAD_FLOW.md +300 -0
  21. package/docs/images/forloop-opencode-flow.png +0 -0
  22. package/docs/images/forloop-opencode-header.png +0 -0
  23. package/docs/superpowers/plans/2026-05-03-sprint-isolated-directory-structure.md +1450 -0
  24. package/docs/superpowers/plans/2026-05-29-install-system.md +850 -0
  25. package/docs/superpowers/specs/2026-05-03-sprint-isolated-directory-structure-design.md +149 -0
  26. package/docs/superpowers/specs/2026-05-19-opencode-message-recording-design.md +468 -0
  27. package/docs/superpowers/specs/2026-05-29-install-system-design.md +135 -0
  28. package/install.sh +317 -0
  29. package/package.json +87 -0
  30. package/plugins/capabilities/api-client.ts +556 -0
  31. package/plugins/capabilities/auth.ts +120 -0
  32. package/plugins/capabilities/config.ts +195 -0
  33. package/plugins/capabilities/context-resolver.ts +106 -0
  34. package/plugins/capabilities/token-storage.ts +98 -0
  35. package/plugins/forloop-plugin.ts +186 -0
  36. package/plugins/hooks/message-recorder.ts +258 -0
  37. package/plugins/hooks/shell-env.ts +62 -0
  38. package/plugins/tools/agent-sprint-tools.ts +100 -0
  39. package/plugins/tools/agent-suggestion-tools.ts +275 -0
  40. package/plugins/tools/agent-tools.ts +114 -0
  41. package/plugins/tools/ai-trigger-tools.ts +79 -0
  42. package/plugins/tools/developer-status-tool.ts +97 -0
  43. package/plugins/tools/file-tools.ts +240 -0
  44. package/plugins/tools/folder-tools.ts +75 -0
  45. package/plugins/tools/organization-tools.ts +213 -0
  46. package/plugins/tools/schedule-tools.ts +173 -0
  47. package/plugins/tools/sprint-tools.ts +287 -0
  48. package/plugins/tools/story-tools.ts +320 -0
  49. package/plugins/tools/sync-tools.ts +509 -0
  50. package/plugins/tools/template-tools.ts +170 -0
  51. package/plugins/tools/user-tools.ts +146 -0
  52. package/scripts/migrate-tokens.js +90 -0
  53. package/scripts/use-prod.sh +11 -0
  54. package/tsconfig.json +17 -0
  55. package/update.sh +172 -0
@@ -0,0 +1,195 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ export const PROD_API_URL = 'https://api.forloop.cc';
5
+ export const DEV_API_URL = 'https://api.dev.forloop.cc';
6
+
7
+ export const PROD_TOKENS_URL = 'https://forloop.cc/profile?tab=api-tokens';
8
+ export const DEV_TOKENS_URL = 'https://dev.forloop.cc/profile?tab=api-tokens';
9
+
10
+ export type Environment = 'development' | 'production' | 'custom';
11
+
12
+ export interface ForLoopConfig {
13
+ apiUrl: string;
14
+ environment: Environment;
15
+ tokenStorage: 'global' | 'project';
16
+ debug: boolean;
17
+ allowDev: boolean;
18
+ }
19
+
20
+ type JsonObject = Record<string, unknown>;
21
+
22
+ const readJsonFileIfExists = (filePath: string): JsonObject | null => {
23
+ try {
24
+ if (!fs.existsSync(filePath)) return null;
25
+ const raw = fs.readFileSync(filePath, 'utf-8');
26
+ return JSON.parse(raw) as JsonObject;
27
+ } catch {
28
+ return null;
29
+ }
30
+ };
31
+
32
+ const getHomeDir = (): string => {
33
+ return process.env.HOME || process.env.USERPROFILE || '';
34
+ };
35
+
36
+ const getProjectConfig = (): JsonObject | null => {
37
+ return readJsonFileIfExists(path.join(process.cwd(), 'opencode.json'));
38
+ };
39
+
40
+ const getGlobalConfig = (): JsonObject | null => {
41
+ const home = getHomeDir();
42
+ if (!home) return null;
43
+ return readJsonFileIfExists(path.join(home, '.config', 'opencode', 'config.json'));
44
+ };
45
+
46
+ const getNested = (obj: JsonObject | null, keys: string[]): unknown => {
47
+ let cur: unknown = obj;
48
+ for (const key of keys) {
49
+ if (!cur || typeof cur !== 'object') return undefined;
50
+ cur = (cur as JsonObject)[key];
51
+ }
52
+ return cur;
53
+ };
54
+
55
+ const isTruthy = (value: unknown): boolean => {
56
+ if (typeof value === 'boolean') return value;
57
+ if (typeof value === 'string') return value.toLowerCase() === 'true' || value === '1';
58
+ return false;
59
+ };
60
+
61
+ const normalizeUrl = (value: unknown): string | null => {
62
+ if (typeof value !== 'string') return null;
63
+ const trimmed = value.trim();
64
+ if (!trimmed) return null;
65
+ return trimmed;
66
+ };
67
+
68
+ const isDevUrl = (apiUrl: string): boolean => {
69
+ return apiUrl.includes('api.dev.forloop.cc') || apiUrl.includes('dev.forloop.cc');
70
+ };
71
+
72
+ const resolveAllowDev = (projectConfig: JsonObject | null, globalConfig: JsonObject | null): boolean => {
73
+ return (
74
+ isTruthy(process.env.FORLOOP_ALLOW_DEV) ||
75
+ isTruthy(getNested(projectConfig, ['forloop', 'allowDev'])) ||
76
+ isTruthy(getNested(globalConfig, ['forloop', 'allowDev']))
77
+ );
78
+ };
79
+
80
+ const resolveApiUrl = (projectConfig: JsonObject | null, globalConfig: JsonObject | null, allowDev: boolean): string => {
81
+ const envApiUrl = normalizeUrl(process.env.FORLOOP_API_URL || process.env.FORLOOP_API_BASE);
82
+ if (envApiUrl) {
83
+ if (isDevUrl(envApiUrl) && !allowDev) {
84
+ console.warn('[ForLoop Config] Dev API URL ignored (set FORLOOP_ALLOW_DEV=true to enable)');
85
+ return PROD_API_URL;
86
+ }
87
+ return envApiUrl;
88
+ }
89
+
90
+ const envName = normalizeUrl(process.env.FORLOOP_ENV);
91
+ if (envName) {
92
+ if (envName === 'production') return PROD_API_URL;
93
+ if (envName === 'development') {
94
+ if (!allowDev) {
95
+ console.warn('[ForLoop Config] FORLOOP_ENV=development ignored (set FORLOOP_ALLOW_DEV=true to enable)');
96
+ return PROD_API_URL;
97
+ }
98
+ return DEV_API_URL;
99
+ }
100
+ }
101
+
102
+ const projectApiUrl = normalizeUrl(getNested(projectConfig, ['forloop', 'apiUrl']));
103
+ if (projectApiUrl) {
104
+ if (isDevUrl(projectApiUrl) && !allowDev) {
105
+ console.warn('[ForLoop Config] Project dev API URL ignored (set forloop.allowDev=true or FORLOOP_ALLOW_DEV=true)');
106
+ return PROD_API_URL;
107
+ }
108
+ return projectApiUrl;
109
+ }
110
+
111
+ const globalApiUrl = normalizeUrl(getNested(globalConfig, ['forloop', 'apiUrl']));
112
+ if (globalApiUrl) {
113
+ if (isDevUrl(globalApiUrl) && !allowDev) {
114
+ console.warn('[ForLoop Config] Global dev API URL ignored (set forloop.allowDev=true or FORLOOP_ALLOW_DEV=true)');
115
+ return PROD_API_URL;
116
+ }
117
+ return globalApiUrl;
118
+ }
119
+
120
+ return PROD_API_URL;
121
+ };
122
+
123
+ const detectEnvironment = (apiUrl: string): Environment => {
124
+ if (apiUrl.includes('dev.forloop.cc')) {
125
+ return 'development';
126
+ }
127
+ if (apiUrl.includes('api.forloop.cc')) {
128
+ return 'production';
129
+ }
130
+ return 'custom';
131
+ };
132
+
133
+ export const getConfig = (): ForLoopConfig => {
134
+ const projectConfig = getProjectConfig();
135
+ const globalConfig = getGlobalConfig();
136
+ const allowDev = resolveAllowDev(projectConfig, globalConfig);
137
+ const apiUrl = resolveApiUrl(projectConfig, globalConfig, allowDev);
138
+
139
+ return {
140
+ apiUrl,
141
+ environment: detectEnvironment(apiUrl),
142
+ tokenStorage: 'global',
143
+ debug: process.env.FORLOOP_DEBUG === 'true',
144
+ allowDev,
145
+ };
146
+ };
147
+
148
+ export const isDevelopment = (): boolean => {
149
+ const config = getConfig();
150
+ return config.environment === 'development' || process.env.NODE_ENV === 'development';
151
+ };
152
+
153
+ export const isProduction = (): boolean => {
154
+ const config = getConfig();
155
+ return config.environment === 'production';
156
+ };
157
+
158
+ export const printConfig = (): void => {
159
+ const config = getConfig();
160
+ console.log('[ForLoop Config]', {
161
+ apiUrl: config.apiUrl,
162
+ environment: config.environment,
163
+ debug: config.debug,
164
+ allowDev: config.allowDev,
165
+ });
166
+ };
167
+
168
+ export const getTokensUrl = (): string => {
169
+ const config = getConfig();
170
+ if (config.environment === 'development') return DEV_TOKENS_URL;
171
+ return PROD_TOKENS_URL;
172
+ };
173
+
174
+ /**
175
+ * Detect if running in AWS Lambda environment
176
+ * Uses standard Lambda environment variables
177
+ */
178
+ export const isLambdaExecution = (): boolean => {
179
+ const forced = (process.env.FORLOOP_EXECUTION_CONTEXT || '').toLowerCase();
180
+ if (forced === 'lambda' || forced === 'aws_lambda' || forced === 'aws-lambda') return true;
181
+ return !!(
182
+ process.env.AWS_LAMBDA_FUNCTION_NAME ||
183
+ process.env.AWS_EXECUTION_ENV ||
184
+ process.env.LAMBDA_TASK_ROOT
185
+ );
186
+ };
187
+
188
+ /**
189
+ * Get the execution context (local or lambda)
190
+ */
191
+ export type ExecutionContext = 'local' | 'lambda';
192
+
193
+ export const getExecutionContext = (): ExecutionContext => {
194
+ return isLambdaExecution() ? 'lambda' : 'local';
195
+ };
@@ -0,0 +1,106 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+
6
+ const execAsync = promisify(exec);
7
+
8
+ export interface ContextResolution {
9
+ sprintId?: number;
10
+ branch?: string;
11
+ source: 'provided' | 'env' | 'branch' | 'manifest' | 'config';
12
+ }
13
+
14
+ export async function getCurrentBranch(directory?: string): Promise<string> {
15
+ try {
16
+ const result = await execAsync('git rev-parse --abbrev-ref HEAD', {
17
+ cwd: directory,
18
+ });
19
+ return result.stdout.trim();
20
+ } catch (error) {
21
+ throw new Error('Git not available or not in a repository');
22
+ }
23
+ }
24
+
25
+ export async function resolveSprintFromBranch(branch: string): Promise<number | null> {
26
+ const match = branch.match(/sprint-(\d+)/);
27
+ if (match) {
28
+ return parseInt(match[1], 10);
29
+ }
30
+ return null;
31
+ }
32
+
33
+ export async function resolveSprintId(
34
+ providedId?: number,
35
+ directory?: string
36
+ ): Promise<ContextResolution> {
37
+ if (providedId) {
38
+ return { sprintId: providedId, source: 'provided' };
39
+ }
40
+
41
+ const envSprintId = process.env.FORLOOP_SPRINT_ID;
42
+ if (envSprintId) {
43
+ return { sprintId: parseInt(envSprintId, 10), source: 'env' };
44
+ }
45
+
46
+ const manifestSprintId = getActiveSprintIdFromManifest(directory);
47
+ if (manifestSprintId) {
48
+ return { sprintId: manifestSprintId, source: 'manifest' };
49
+ }
50
+
51
+ try {
52
+ const branch = await getCurrentBranch(directory);
53
+ const sprintId = await resolveSprintFromBranch(branch);
54
+ if (sprintId) {
55
+ return { sprintId, branch, source: 'branch' };
56
+ }
57
+ } catch (error) {
58
+ }
59
+
60
+ return { source: 'config' };
61
+ }
62
+
63
+ function getActiveSprintIdFromManifest(directory?: string): number | null {
64
+ const candidates: string[] = [];
65
+ if (directory) {
66
+ candidates.push(path.join(directory, '.forloop', 'manifest.json'));
67
+ }
68
+
69
+ const home = process.env.HOME;
70
+ if (home) {
71
+ candidates.push(path.join(home, '.forloop', 'manifest.json'));
72
+ }
73
+
74
+ for (const filePath of candidates) {
75
+ try {
76
+ if (!fs.existsSync(filePath)) continue;
77
+ const raw = fs.readFileSync(filePath, 'utf8');
78
+ const parsed = JSON.parse(raw);
79
+ const id = parsed?.activeSprintId;
80
+ if (Number.isFinite(id)) return Number(id);
81
+ } catch {
82
+ }
83
+ }
84
+
85
+ return null;
86
+ }
87
+
88
+ export const DEFAULT_HOME = '/tmp/home';
89
+
90
+ export function getSprintDir(sprintId: number): string {
91
+ return `sprint-${sprintId}`;
92
+ }
93
+
94
+ export function getForloopRoot(sprintId?: number): string {
95
+ const home = process.env.HOME || DEFAULT_HOME;
96
+ const root = path.join(home, '.forloop');
97
+ if (sprintId !== undefined) {
98
+ return path.join(root, getSprintDir(sprintId));
99
+ }
100
+ return root;
101
+ }
102
+
103
+ export function getManifestPath(): string {
104
+ const home = process.env.HOME || DEFAULT_HOME;
105
+ return path.join(home, '.forloop', 'manifest.json');
106
+ }
@@ -0,0 +1,98 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+
4
+ export interface TokenProfile {
5
+ version: string; // Schema version for future migrations
6
+ default?: string;
7
+ profiles?: Record<string, string>;
8
+ lastUpdated: string;
9
+ note: string; // Clear indication this is ForLoop-specific
10
+ }
11
+
12
+ const getTokenPath = (): string => {
13
+ return path.join(
14
+ process.env.HOME || process.env.USERPROFILE || '.',
15
+ '.config',
16
+ 'forloop',
17
+ 'tokens.json'
18
+ );
19
+ };
20
+
21
+ export async function loadTokens(): Promise<TokenProfile> {
22
+ const tokenPath = getTokenPath();
23
+
24
+ try {
25
+ const content = await fs.readFile(tokenPath, 'utf-8');
26
+ return JSON.parse(content);
27
+ } catch (error: any) {
28
+ if (error.code === 'ENOENT') {
29
+ return {
30
+ version: '1.0',
31
+ note: 'ForLoop API tokens - do not share',
32
+ lastUpdated: new Date().toISOString()
33
+ };
34
+ }
35
+ throw error;
36
+ }
37
+ }
38
+
39
+ export async function saveTokens(tokens: TokenProfile): Promise<void> {
40
+ const tokenPath = getTokenPath();
41
+ const configDir = path.dirname(tokenPath);
42
+
43
+ await fs.mkdir(configDir, { recursive: true });
44
+ await fs.writeFile(
45
+ tokenPath,
46
+ JSON.stringify(tokens, null, 2),
47
+ { mode: 0o600 }
48
+ );
49
+ }
50
+
51
+ export async function getToken(profile?: string): Promise<string | null> {
52
+ const tokens = await loadTokens();
53
+
54
+ if (profile && tokens.profiles?.[profile]) {
55
+ return tokens.profiles[profile];
56
+ }
57
+
58
+ // Check top-level default first, then fall back to profiles["default"]
59
+ if (tokens.default) return tokens.default;
60
+ if (tokens.profiles?.['default']) return tokens.profiles['default'];
61
+
62
+ return null;
63
+ }
64
+
65
+ export async function setToken(token: string, profile?: string): Promise<void> {
66
+ const tokens = await loadTokens();
67
+
68
+ // Ensure version and note are set
69
+ tokens.version = '1.0';
70
+ tokens.note = 'ForLoop API tokens - do not share';
71
+
72
+ if (profile) {
73
+ if (!tokens.profiles) {
74
+ tokens.profiles = {};
75
+ }
76
+ tokens.profiles[profile] = token;
77
+ } else {
78
+ tokens.default = token;
79
+ }
80
+
81
+ tokens.lastUpdated = new Date().toISOString();
82
+ await saveTokens(tokens);
83
+ }
84
+
85
+ export async function clearToken(profile?: string): Promise<void> {
86
+ const tokens = await loadTokens();
87
+
88
+ if (profile) {
89
+ if (tokens.profiles) {
90
+ delete tokens.profiles[profile];
91
+ }
92
+ } else {
93
+ delete tokens.default;
94
+ }
95
+
96
+ tokens.lastUpdated = new Date().toISOString();
97
+ await saveTokens(tokens);
98
+ }
@@ -0,0 +1,186 @@
1
+ import { getToken } from './capabilities/token-storage';
2
+ import { ForLoopAPIClient } from './capabilities/api-client';
3
+ import { getConfig, isLambdaExecution } from './capabilities/config';
4
+ import {
5
+ createSprintGetTool,
6
+ createSprintListTool,
7
+ createSprintCreateTool,
8
+ createSprintUpdateTool,
9
+ createSprintDeleteTool,
10
+ } from './tools/sprint-tools';
11
+ import {
12
+ createStoryCreateTool,
13
+ createStoryUpdateTool,
14
+ createStoryDeleteTool,
15
+ createStoryGetTool,
16
+ } from './tools/story-tools';
17
+ import {
18
+ createTemplateListTool,
19
+ createStoryFromTemplateTool,
20
+ } from './tools/template-tools';
21
+ import {
22
+ createUserProfileTool,
23
+ createUserQuotasTool,
24
+ createOrganizationQuotasTool,
25
+ } from './tools/user-tools';
26
+ import {
27
+ createOrganizationListTool,
28
+ createOrganizationGetTool,
29
+ createOrganizationCreateTool,
30
+ createOrganizationUpdateTool,
31
+ createOrganizationDeleteTool,
32
+ } from './tools/organization-tools';
33
+ import {
34
+ createFileUploadTool,
35
+ createFileListTool,
36
+ createFileDeleteTool,
37
+ createFileDownloadUrlTool,
38
+ } from './tools/file-tools';
39
+ import {
40
+ createScheduleTool,
41
+ createScheduleUpdateTool,
42
+ } from './tools/schedule-tools';
43
+ import {
44
+ createDocFolderTool,
45
+ } from './tools/folder-tools';
46
+ import {
47
+ createAivyDocFolderEnsureTool,
48
+ createAivyDocFolderGetTool,
49
+ createS3ToLocalSyncTool,
50
+ createLocalToS3SyncTool,
51
+ } from './tools/sync-tools';
52
+ import { createAgentQueryTool, createTokenSetTool, createTokenGetTool } from './tools/agent-tools';
53
+ import {
54
+ createAgentSuggestTool,
55
+ createStoryBreakdownTool,
56
+ createStoryEstimateTool,
57
+ createConversationHistoryTool,
58
+ createClearHistoryTool
59
+ } from './tools/agent-suggestion-tools';
60
+ import {
61
+ createAgentListTool,
62
+ createSprintAiAgentsUpdateTool,
63
+ } from './tools/agent-sprint-tools';
64
+ import { createAITriggerTools } from './tools/ai-trigger-tools';
65
+ import { createDeveloperStatusTool } from './tools/developer-status-tool';
66
+ import { injectEnvironment } from './hooks/shell-env';
67
+ import { createChatMessageHook, createEventHook } from './hooks/message-recorder';
68
+
69
+ export default async (ctx: any) => {
70
+ const config = getConfig();
71
+ const token = (await getToken()) || '';
72
+
73
+ console.log(
74
+ `[ForLoop] Plugin initialized - ${config.environment} (${config.apiUrl}) execution=${isLambdaExecution() ? 'lambda' : 'local'} forced=${process.env.FORLOOP_EXECUTION_CONTEXT || 'not set'}`
75
+ );
76
+ if (!token) {
77
+ console.log('[ForLoop] No token configured - tools will ask you to set one');
78
+ }
79
+
80
+ const client = new ForLoopAPIClient({
81
+ token,
82
+ baseUrl: config.apiUrl,
83
+ });
84
+
85
+ const aiTriggerTools = createAITriggerTools(client);
86
+
87
+ return {
88
+ tool: {
89
+ // Token Management
90
+ 'forloopTokenSet': createTokenSetTool(client),
91
+ 'forloopTokenGet': createTokenGetTool(),
92
+
93
+ // Sprint Operations
94
+ 'forloopSprintList': createSprintListTool(client),
95
+ 'forloopSprintGet': createSprintGetTool(client),
96
+ 'forloopSprintCreate': createSprintCreateTool(client),
97
+ 'forloopSprintUpdate': createSprintUpdateTool(client),
98
+ 'forloopSprintDelete': createSprintDeleteTool(client),
99
+
100
+ // Story Operations
101
+ 'forloopStoryCreate': createStoryCreateTool(client),
102
+ 'forloopStoryGet': createStoryGetTool(client),
103
+ 'forloopStoryUpdate': createStoryUpdateTool(client),
104
+ 'forloopStoryDelete': createStoryDeleteTool(client),
105
+
106
+ // Template Operations
107
+ 'forloopTemplateList': createTemplateListTool(client),
108
+ 'forloopStoryTemplate': createStoryFromTemplateTool(client),
109
+
110
+ // User & Quota Operations
111
+ 'forloopUserProfile': createUserProfileTool(client),
112
+ 'forloopUserQuotas': createUserQuotasTool(client),
113
+ 'forloopOrganizationQuotas': createOrganizationQuotasTool(client),
114
+
115
+ // Organization Management
116
+ 'forloopOrganizationList': createOrganizationListTool(client),
117
+ 'forloopOrganizationGet': createOrganizationGetTool(client),
118
+ 'forloopOrganizationCreate': createOrganizationCreateTool(client),
119
+ 'forloopOrganizationUpdate': createOrganizationUpdateTool(client),
120
+ 'forloopOrganizationDelete': createOrganizationDeleteTool(client),
121
+
122
+ // File Upload & Management
123
+ 'forloopFileUpload': createFileUploadTool(client),
124
+ 'forloopFileList': createFileListTool(client),
125
+ 'forloopFileDelete': createFileDeleteTool(client),
126
+ 'forloopFileDownload': createFileDownloadUrlTool(client),
127
+
128
+ // Schedule Meetings
129
+ 'forloopScheduleCreate': createScheduleTool(client),
130
+ 'forloopScheduleUpdate': createScheduleUpdateTool(client),
131
+
132
+ // Document Folders
133
+ 'forloopDocFolder': createDocFolderTool(client),
134
+
135
+ // Aivy Doc Sync
136
+ 'forloopSyncAivyFolder': createAivyDocFolderEnsureTool(client),
137
+ 'forloopAivyDocGet': createAivyDocFolderGetTool(client),
138
+ 'forloopSyncS3ToLocal': createS3ToLocalSyncTool(client),
139
+ 'forloopSyncLocalToS3': createLocalToS3SyncTool(client),
140
+
141
+ // AI Agent Tools
142
+ 'forloopAgentQuery': createAgentQueryTool(client),
143
+ 'forloopAgentSuggest': createAgentSuggestTool(client),
144
+ 'forloopAgentBreakdown': createStoryBreakdownTool(client),
145
+ 'forloopAgentEstimate': createStoryEstimateTool(client),
146
+ 'forloopAgentHistory': createConversationHistoryTool(client),
147
+ 'forloopAgentClear': createClearHistoryTool(client),
148
+ 'forloopAiAgentList': createAgentListTool(client),
149
+ 'forloopSprintAiAgentsUpdate': createSprintAiAgentsUpdateTool(client),
150
+
151
+ // AI Execution Tools
152
+ ...aiTriggerTools,
153
+ 'forloopDeveloperStatus': createDeveloperStatusTool(client),
154
+ },
155
+ event: createEventHook(client),
156
+ 'chat.message': createChatMessageHook(client),
157
+ 'shell.env': injectEnvironment,
158
+ 'tool.execute.before': (input: any, output: any) => {
159
+ if (!(globalThis as any).__toolHookKeysLogged) {
160
+ (globalThis as any).__toolHookKeysLogged = 1;
161
+ console.log('[ForLoop] tool.execute.before input keys:', Object.keys(input || {}).join(', '));
162
+ }
163
+ const agent = input.agent || input.agentName || input.sourceAgent || input.agentId || '';
164
+ const args = typeof output?.args === 'object' ? JSON.stringify(output.args) : String(output?.args || '');
165
+ console.log(`[forloop][${agent}] ▶ ${input.tool}`, args.substring(0, 400));
166
+ },
167
+ 'tool.execute.after': (input: any, output: any) => {
168
+ const agent = input.agent || input.agentName || input.sourceAgent || input.agentId || '';
169
+ const summary = typeof output === 'string'
170
+ ? output.substring(0, 200)
171
+ : (output?.text || output?.message || JSON.stringify(output).substring(0, 200));
172
+ console.log(`[forloop][${agent}] ✓ ${input.tool} completed:`, summary);
173
+ },
174
+ };
175
+ };
176
+
177
+ export { ForLoopAPIClient } from './capabilities/api-client';
178
+ export type { APIError, CreateStoryRequest, CreateSprintRequest, ListSprintsParams, SprintOptions } from './capabilities/api-client';
179
+ export { getConfig, isLambdaExecution, PROD_API_URL, DEV_API_URL } from './capabilities/config';
180
+ export type { ForLoopConfig } from './capabilities/config';
181
+ export { getToken, setToken, clearToken } from './capabilities/token-storage';
182
+ export type { TokenProfile } from './capabilities/token-storage';
183
+ export { validateToken, detectTokenType } from './capabilities/auth';
184
+ export type { TokenValidationResult } from './capabilities/auth';
185
+ export { resolveSprintId, getCurrentBranch, resolveSprintFromBranch, getForloopRoot, getManifestPath } from './capabilities/context-resolver';
186
+ export type { ContextResolution } from './capabilities/context-resolver';