@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.
- package/CHANGELOG.md +225 -0
- package/CODE_OF_CONDUCT.md +77 -0
- package/CONTRIBUTING.md +176 -0
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/SECURITY.md +69 -0
- package/docs/CONFIGURATION.md +283 -0
- package/docs/DEVELOPER-VS-USER.md +129 -0
- package/docs/IMPROVED-AUTHENTICATION.md +317 -0
- package/docs/INSTALLATION-SCRIPTS.md +304 -0
- package/docs/PLANNER-AGENT-IMPROVEMENTS.md +88 -0
- package/docs/PLUGIN-ARCHITECTURE.md +308 -0
- package/docs/QUICKSTART-NON-DEV.md +94 -0
- package/docs/README-FOR-HUMANS-AND-AI.md +109 -0
- package/docs/README.md +70 -0
- package/docs/SKILLS-GAP-ANALYSIS.md +157 -0
- package/docs/USER-INTERACTION-MODEL.md +106 -0
- package/docs/file-management/2026-04-12-forloop-aivy-documents-sync.md +395 -0
- package/docs/file-management/FILE_UPLOAD_ENHANCEMENT.md +431 -0
- package/docs/file-management/FILE_UPLOAD_FLOW.md +300 -0
- package/docs/images/forloop-opencode-flow.png +0 -0
- package/docs/images/forloop-opencode-header.png +0 -0
- package/docs/superpowers/plans/2026-05-03-sprint-isolated-directory-structure.md +1450 -0
- package/docs/superpowers/plans/2026-05-29-install-system.md +850 -0
- package/docs/superpowers/specs/2026-05-03-sprint-isolated-directory-structure-design.md +149 -0
- package/docs/superpowers/specs/2026-05-19-opencode-message-recording-design.md +468 -0
- package/docs/superpowers/specs/2026-05-29-install-system-design.md +135 -0
- package/install.sh +317 -0
- package/package.json +87 -0
- package/plugins/capabilities/api-client.ts +556 -0
- package/plugins/capabilities/auth.ts +120 -0
- package/plugins/capabilities/config.ts +195 -0
- package/plugins/capabilities/context-resolver.ts +106 -0
- package/plugins/capabilities/token-storage.ts +98 -0
- package/plugins/forloop-plugin.ts +186 -0
- package/plugins/hooks/message-recorder.ts +258 -0
- package/plugins/hooks/shell-env.ts +62 -0
- package/plugins/tools/agent-sprint-tools.ts +100 -0
- package/plugins/tools/agent-suggestion-tools.ts +275 -0
- package/plugins/tools/agent-tools.ts +114 -0
- package/plugins/tools/ai-trigger-tools.ts +79 -0
- package/plugins/tools/developer-status-tool.ts +97 -0
- package/plugins/tools/file-tools.ts +240 -0
- package/plugins/tools/folder-tools.ts +75 -0
- package/plugins/tools/organization-tools.ts +213 -0
- package/plugins/tools/schedule-tools.ts +173 -0
- package/plugins/tools/sprint-tools.ts +287 -0
- package/plugins/tools/story-tools.ts +320 -0
- package/plugins/tools/sync-tools.ts +509 -0
- package/plugins/tools/template-tools.ts +170 -0
- package/plugins/tools/user-tools.ts +146 -0
- package/scripts/migrate-tokens.js +90 -0
- package/scripts/use-prod.sh +11 -0
- package/tsconfig.json +17 -0
- 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';
|