@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,146 @@
1
+ import { tool } from '@opencode-ai/plugin';
2
+ import { ForLoopAPIClient } from '../capabilities/api-client';
3
+ import { validateToken } from '../capabilities/auth';
4
+
5
+ export function createUserProfileTool(client: ForLoopAPIClient) {
6
+ return tool({
7
+ description: 'Get current user profile information (name, email, tier)',
8
+ args: {},
9
+ async execute(_args, _context) {
10
+ const tokenResult = await validateToken();
11
+ if (!tokenResult.valid) {
12
+ return tokenResult.error;
13
+ }
14
+ client.setToken?.(tokenResult.token);
15
+
16
+ try {
17
+ const profile = await client.getUserProfile();
18
+
19
+ const lines: string[] = [
20
+ `👤 User Profile`,
21
+ '',
22
+ `**Name**: ${profile.name || 'Not set'}`,
23
+ `**Email**: ${profile.email}`,
24
+ `**Tier**: ${profile.tier || 'free'}`,
25
+ ];
26
+
27
+ if (profile.bio) {
28
+ lines.push(`**Bio**: ${profile.bio}`);
29
+ }
30
+
31
+ if (profile.avatarUrl) {
32
+ lines.push(`**Avatar**: ${profile.avatarUrl}`);
33
+ }
34
+
35
+ return lines.join('\n');
36
+ } catch (error: any) {
37
+ return `❌ Error: ${error.message}`;
38
+ }
39
+ },
40
+ });
41
+ }
42
+
43
+ export function createUserQuotasTool(client: ForLoopAPIClient) {
44
+ return tool({
45
+ description: 'Check user quota limits and usage (organizations, sprints, storage)',
46
+ args: {},
47
+ async execute(_args, _context) {
48
+ const tokenResult = await validateToken();
49
+ if (!tokenResult.valid) {
50
+ return tokenResult.error;
51
+ }
52
+ client.setToken?.(tokenResult.token);
53
+
54
+ try {
55
+ const quotas = await client.getUserQuotas();
56
+
57
+ const formatBytes = (bytes: string) => {
58
+ const num = parseInt(bytes, 10);
59
+ if (num === 0) return '0 Bytes';
60
+ const k = 1024;
61
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
62
+ const i = Math.floor(Math.log(num) / Math.log(k));
63
+ return parseFloat((num / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
64
+ };
65
+
66
+ const lines: string[] = [
67
+ `📊 Quota Usage (${quotas.tier || 'free'} tier)`,
68
+ '',
69
+ ];
70
+
71
+ // Organizations
72
+ lines.push(`**Organizations**: ${quotas.remaining?.ownedOrganizations ?? '?'}/${quotas.limits?.maxOwnedOrganizations ?? '?'}`);
73
+
74
+ // System Sprints
75
+ lines.push(`**System Sprints**: ${quotas.remaining?.systemSpaces ?? '?'}/${quotas.limits?.maxSystemSprints ?? '?'}`);
76
+
77
+ // Storage
78
+ const storageUsed = formatBytes(quotas.usage?.storageUsedBytes || '0');
79
+ const storageLimit = formatBytes(quotas.limits?.maxStorageBytes || '0');
80
+ lines.push(`**Storage**: ${storageUsed} / ${storageLimit}`);
81
+
82
+ // Free tier stories
83
+ if (quotas.tier === 'free') {
84
+ lines.push(`**Free Stories**: ${quotas.remaining?.freeStories ?? '?'}/${quotas.limits?.maxStoriesPerFreeSystemSprint ?? '?'}`);
85
+ }
86
+
87
+ // Owned organizations usage
88
+ if (quotas.ownedOrgsUsage && quotas.ownedOrgsUsage.length > 0) {
89
+ lines.push('');
90
+ lines.push('**Owned Organizations:**');
91
+ for (const org of quotas.ownedOrgsUsage) {
92
+ lines.push(` - ${org.name}: ${org.spacesUsed}/${org.spacesLimit} sprints`);
93
+ }
94
+ }
95
+
96
+ return lines.join('\n');
97
+ } catch (error: any) {
98
+ return `❌ Error: ${error.message}`;
99
+ }
100
+ },
101
+ });
102
+ }
103
+
104
+ export function createOrganizationQuotasTool(client: ForLoopAPIClient) {
105
+ return tool({
106
+ description: 'Get quota information for a specific organization',
107
+ args: {
108
+ organizationId: tool.schema.number()
109
+ .describe('Organization ID to check quotas for'),
110
+ },
111
+ async execute(args, _context) {
112
+ const tokenResult = await validateToken();
113
+ if (!tokenResult.valid) {
114
+ return tokenResult.error;
115
+ }
116
+ client.setToken?.(tokenResult.token);
117
+
118
+ try {
119
+ const quotas = await client.getOrganizationQuotas(args.organizationId);
120
+
121
+ const lines: string[] = [
122
+ `📊 Organization Quotas`,
123
+ '',
124
+ `**Organization**: ${quotas.organization?.name || `#${quotas.organization?.id}`}`,
125
+ `**Type**: ${quotas.organization?.type || 'user'}`,
126
+ ];
127
+
128
+ if (quotas.limits) {
129
+ lines.push('');
130
+ lines.push('**Limits:**');
131
+ lines.push(` - Storage: ${parseInt(quotas.limits.maxStorageBytes).toLocaleString()} bytes`);
132
+ }
133
+
134
+ if (quotas.usage) {
135
+ lines.push('');
136
+ lines.push('**Usage:**');
137
+ lines.push(` - Sprints: ${quotas.usage.spacesUsed}`);
138
+ }
139
+
140
+ return lines.join('\n');
141
+ } catch (error: any) {
142
+ return `❌ Error: ${error.message}`;
143
+ }
144
+ },
145
+ });
146
+ }
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ForLoop Token Migration Script
4
+ *
5
+ * Migrates tokens from old location (~/.config/opencode/tokens.json)
6
+ * to new location (~/.config/forloop/tokens.json)
7
+ */
8
+
9
+ import { promises as fs } from 'fs';
10
+ import path from 'path';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+
15
+ const OLD_TOKEN_PATH = path.join(
16
+ process.env.HOME || process.env.USERPROFILE || '.',
17
+ '.config',
18
+ 'opencode',
19
+ 'tokens.json'
20
+ );
21
+
22
+ const NEW_TOKEN_PATH = path.join(
23
+ process.env.HOME || process.env.USERPROFILE || '.',
24
+ '.config',
25
+ 'forloop',
26
+ 'tokens.json'
27
+ );
28
+
29
+ async function migrate() {
30
+ console.log('🔄 ForLoop Token Migration');
31
+ console.log('=========================\n');
32
+
33
+ // Check if old file exists
34
+ try {
35
+ await fs.access(OLD_TOKEN_PATH);
36
+ } catch (error) {
37
+ if (error.code === 'ENOENT') {
38
+ console.log('✅ No old token file found - nothing to migrate');
39
+ console.log(`\nTokens will be stored at: ${NEW_TOKEN_PATH}`);
40
+ return;
41
+ }
42
+ throw error;
43
+ }
44
+
45
+ console.log(`📂 Old location: ${OLD_TOKEN_PATH}`);
46
+ console.log(`📂 New location: ${NEW_TOKEN_PATH}`);
47
+ console.log('');
48
+
49
+ // Read old tokens
50
+ const content = await fs.readFile(OLD_TOKEN_PATH, 'utf-8');
51
+ const oldTokens = JSON.parse(content);
52
+
53
+ // Create new token structure
54
+ const newTokens = {
55
+ version: '1.0',
56
+ note: 'ForLoop API tokens - do not share',
57
+ default: oldTokens.default,
58
+ profiles: oldTokens.profiles || {},
59
+ lastUpdated: oldTokens.lastUpdated || new Date().toISOString(),
60
+ };
61
+
62
+ // Create new directory
63
+ const newDir = path.dirname(NEW_TOKEN_PATH);
64
+ await fs.mkdir(newDir, { recursive: true });
65
+
66
+ // Write new file with secure permissions
67
+ await fs.writeFile(NEW_TOKEN_PATH, JSON.stringify(newTokens, null, 2), {
68
+ mode: 0o600, // Owner read/write only
69
+ });
70
+
71
+ console.log('✅ Migration successful!');
72
+ console.log('');
73
+ console.log('📊 Migrated:');
74
+ if (newTokens.default) {
75
+ const masked = newTokens.default.slice(0, 10) + '...';
76
+ console.log(` - Default token: ${masked}`);
77
+ }
78
+ if (newTokens.profiles && Object.keys(newTokens.profiles).length > 0) {
79
+ console.log(` - Profiles: ${Object.keys(newTokens.profiles).join(', ')}`);
80
+ }
81
+ console.log('');
82
+ console.log('📝 You can safely delete the old file:');
83
+ console.log(` rm ${OLD_TOKEN_PATH}`);
84
+ console.log('');
85
+ }
86
+
87
+ migrate().catch(error => {
88
+ console.error('❌ Migration failed:', error.message);
89
+ process.exit(1);
90
+ });
@@ -0,0 +1,11 @@
1
+ #!/bin/bash
2
+ # Quick script to switch to production environment
3
+
4
+ rm -f .env
5
+
6
+ echo "✅ Production environment configured"
7
+ echo ""
8
+ echo "API URL: https://api.forloop.cc (default)"
9
+ echo ""
10
+ echo "To test: opencode"
11
+ echo "Or restart opencode to use default config"
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "esModuleInterop": true,
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "declaration": true,
10
+ "outDir": "./dist",
11
+ "rootDir": ".",
12
+ "resolveJsonModule": true,
13
+ "allowSyntheticDefaultImports": true
14
+ },
15
+ "include": ["./plugins/**/*"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }
package/update.sh ADDED
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # ForLoop Plugin Updater
5
+ # https://github.com/forloop-cc/forloop-opencode-plugin-planner
6
+ #
7
+ # opencode auto-updates plugins on startup. This script verifies the plugin
8
+ # is registered and shows the latest changelog.
9
+ #
10
+ # Usage:
11
+ # bash update.sh
12
+ # bash update.sh --changelog
13
+
14
+ # ── Colors ───────────────────────────────────────────────────────────────────
15
+ PLATFORM="$(uname -s)"
16
+ if [[ "$PLATFORM" =~ MINGW|MSYS|CYGWIN ]] && [ -z "$WT_SESSION" ] && [ -z "$ConEmuPID" ]; then
17
+ RED=''; GREEN=''; YELLOW=''; BLUE=''; CYAN=''; BOLD=''; NC=''
18
+ else
19
+ RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
20
+ BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
21
+ fi
22
+
23
+ # ── Configuration ────────────────────────────────────────────────────────────
24
+ NPM_PACKAGE="@forloop-cc/forloop-opencode-plugin-planner"
25
+ GIT_URL="forloop-planner@git+https://github.com/forloop-cc/forloop-opencode-plugin-planner.git"
26
+ PLUGIN_DIRNAME="forloop-planner"
27
+
28
+ SHOW_CHANGELOG=false
29
+
30
+ print_success() { echo -e "${GREEN}✓${NC} $1"; }
31
+ print_info() { echo -e "${BLUE}ℹ${NC} $1"; }
32
+ print_warning() { echo -e "${YELLOW}⚠${NC} $1"; }
33
+ print_error() { echo -e "${RED}✗${NC} $1" >&2; }
34
+
35
+ show_help() {
36
+ cat << 'HELPEOF'
37
+ Usage: update.sh [OPTIONS]
38
+
39
+ opencode auto-updates plugins from git/npm on startup. This script:
40
+ - Verifies the plugin is registered in opencode.json
41
+ - Shows the latest changelog from the plugin source
42
+
43
+ Options:
44
+ -c, --changelog Show recent changelog after verification
45
+ -h, --help Show this help
46
+
47
+ Auto-detection order:
48
+ 1. ./opencode.json (project config)
49
+ 2. ~/.config/opencode/opencode.json (global config)
50
+ HELPEOF
51
+ }
52
+
53
+ # ── Dependency checks ────────────────────────────────────────────────────────
54
+
55
+ check_jq() {
56
+ if ! command -v jq &>/dev/null; then
57
+ print_error "jq is required but not installed"
58
+ return 1
59
+ fi
60
+ return 0
61
+ }
62
+
63
+ # ── Config lookup ────────────────────────────────────────────────────────────
64
+
65
+ find_config() {
66
+ local candidates=("./opencode.json" "$HOME/.config/opencode/opencode.json")
67
+ for config in "${candidates[@]}"; do
68
+ if [ -f "$config" ]; then
69
+ echo "$config"
70
+ return 0
71
+ fi
72
+ done
73
+ return 1
74
+ }
75
+
76
+ find_plugin_entry() {
77
+ local config_file="$1"
78
+ jq -r '.plugin[]? // empty' "$config_file" | grep -E "forloop-opencode-plugin-planner|$NPM_PACKAGE" | head -1
79
+ }
80
+
81
+ # ── Changelog ────────────────────────────────────────────────────────────────
82
+
83
+ show_changelog_from_repo() {
84
+ local changelog_url="https://raw.githubusercontent.com/forloop-cc/forloop-opencode-plugin-planner/main/CHANGELOG.md"
85
+ echo ""
86
+ print_info "Recent changes:"
87
+ echo "────────────────────────────────────────────"
88
+ curl -fsSL "$changelog_url" 2>/dev/null | head -40 || {
89
+ echo " (could not fetch changelog — check https://github.com/forloop-cc/forloop-opencode-plugin-planner/releases)"
90
+ }
91
+ echo "────────────────────────────────────────────"
92
+ }
93
+
94
+ show_changelog_from_cache() {
95
+ local cache_dir="$HOME/.cache/opencode/node_modules"
96
+ local plugin_paths=(
97
+ "$cache_dir/$NPM_PACKAGE/CHANGELOG.md"
98
+ "$cache_dir/$NPM_PACKAGE/plugins/forloop-plugin.ts"
99
+ )
100
+ for candidate in "${plugin_paths[@]}"; do
101
+ local changelog="$(dirname "$candidate")/CHANGELOG.md"
102
+ if [ -f "$changelog" ]; then
103
+ echo ""
104
+ print_info "Recent changes:"
105
+ echo "────────────────────────────────────────────"
106
+ head -40 "$changelog"
107
+ echo "────────────────────────────────────────────"
108
+ return 0
109
+ fi
110
+ done
111
+ return 1
112
+ }
113
+
114
+ # ── Main ─────────────────────────────────────────────────────────────────────
115
+
116
+ while [[ $# -gt 0 ]]; do
117
+ case "$1" in
118
+ -c|--changelog) SHOW_CHANGELOG=true; shift ;;
119
+ -h|--help) show_help; exit 0 ;;
120
+ *) print_error "Unknown option: $1"; show_help; exit 1 ;;
121
+ esac
122
+ done
123
+
124
+ echo ""
125
+ echo -e "${BLUE}${BOLD}╔══════════════════════════════════════════╗"
126
+ echo -e "║ ForLoop Plugin Updater ║"
127
+ echo -e "╚══════════════════════════════════════════╝${NC}"
128
+ echo ""
129
+
130
+ check_jq || exit 1
131
+
132
+ local config_file
133
+ config_file="$(find_config)" || {
134
+ print_error "No opencode.json found"
135
+ echo ""
136
+ echo " Checked:"
137
+ echo " 1. ./opencode.json"
138
+ echo " 2. ~/.config/opencode/opencode.json"
139
+ echo ""
140
+ echo " Install first: curl -fsSL .../install.sh | bash"
141
+ exit 1
142
+ }
143
+
144
+ local plugin_entry
145
+ plugin_entry="$(find_plugin_entry "$config_file")"
146
+
147
+ if [ -z "$plugin_entry" ]; then
148
+ print_error "ForLoop plugin not registered in $config_file"
149
+ echo ""
150
+ echo " Install first: curl -fsSL .../install.sh | bash"
151
+ exit 1
152
+ fi
153
+
154
+ print_success "Plugin registered in: $config_file"
155
+ print_info "Entry: $plugin_entry"
156
+ echo ""
157
+
158
+ # Detect source type
159
+ if echo "$plugin_entry" | grep -q "git+https://"; then
160
+ print_info "Source: Git (opencode will pull latest on startup)"
161
+ elif echo "$plugin_entry" | grep -q "@forloop-cc/"; then
162
+ print_info "Source: npm (opencode will check for updates on startup)"
163
+ fi
164
+
165
+ echo ""
166
+ print_success "To update: restart opencode — it auto-fetches the latest plugin version"
167
+
168
+ if [ "$SHOW_CHANGELOG" = true ]; then
169
+ show_changelog_from_cache || show_changelog_from_repo
170
+ fi
171
+
172
+ echo ""