gsd-opencode 1.20.2 → 1.20.4
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/commands/gsd/gsd-check-profile.md +30 -0
- package/get-shit-done/bin/gsd-oc-commands/allow-read-config.cjs +235 -0
- package/get-shit-done/bin/gsd-oc-commands/check-oc-config-json.cjs +169 -0
- package/get-shit-done/bin/gsd-oc-commands/check-opencode-json.cjs +86 -0
- package/get-shit-done/bin/gsd-oc-commands/get-profile.cjs +117 -0
- package/get-shit-done/bin/gsd-oc-commands/set-profile.cjs +357 -0
- package/get-shit-done/bin/gsd-oc-commands/update-opencode-json.cjs +199 -0
- package/get-shit-done/bin/gsd-oc-commands/validate-models.cjs +75 -0
- package/get-shit-done/bin/gsd-oc-lib/oc-config.cjs +205 -0
- package/get-shit-done/bin/gsd-oc-lib/oc-core.cjs +113 -0
- package/get-shit-done/bin/gsd-oc-lib/oc-models.cjs +133 -0
- package/get-shit-done/bin/gsd-oc-lib/oc-profile-config.cjs +409 -0
- package/get-shit-done/bin/gsd-oc-tools.cjs +136 -0
- package/get-shit-done/bin/lib/oc-config.cjs +200 -0
- package/get-shit-done/bin/lib/oc-core.cjs +114 -0
- package/get-shit-done/bin/lib/oc-models.cjs +133 -0
- package/get-shit-done/bin/test/allow-read-config.test.cjs +262 -0
- package/get-shit-done/bin/test/fixtures/oc-config-invalid.json +14 -0
- package/get-shit-done/bin/test/fixtures/oc-config-valid.json +22 -0
- package/get-shit-done/bin/test/get-profile.test.cjs +447 -0
- package/get-shit-done/bin/test/oc-profile-config.test.cjs +377 -0
- package/get-shit-done/bin/test/pivot-profile.test.cjs +276 -0
- package/get-shit-done/bin/test/set-profile.test.cjs +301 -0
- package/get-shit-done/workflows/oc-check-profile.md +181 -0
- package/get-shit-done/workflows/oc-set-profile.md +98 -234
- package/get-shit-done/workflows/settings.md +4 -3
- package/package.json +2 -2
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* oc-config.cjs — Profile configuration operations for gsd-oc-tools CLI
|
|
3
|
+
*
|
|
4
|
+
* Provides functions for loading profile config and applying model assignments to opencode.json.
|
|
5
|
+
* Follows gsd-tools.cjs architecture pattern.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Valid profile types whitelist
|
|
13
|
+
*/
|
|
14
|
+
const VALID_PROFILES = ['simple', 'smart', 'genius'];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Profile to agent mapping
|
|
18
|
+
* Maps profile keys to opencode.json agent names
|
|
19
|
+
*/
|
|
20
|
+
const PROFILE_AGENT_MAPPING = {
|
|
21
|
+
// Planning agents
|
|
22
|
+
planning: [
|
|
23
|
+
'gsd-planner',
|
|
24
|
+
'gsd-plan-checker',
|
|
25
|
+
'gsd-phase-researcher',
|
|
26
|
+
'gsd-roadmapper',
|
|
27
|
+
'gsd-project-researcher',
|
|
28
|
+
'gsd-research-synthesizer',
|
|
29
|
+
'gsd-codebase-mapper'
|
|
30
|
+
],
|
|
31
|
+
// Execution agents
|
|
32
|
+
execution: [
|
|
33
|
+
'gsd-executor',
|
|
34
|
+
'gsd-debugger'
|
|
35
|
+
],
|
|
36
|
+
// Verification agents
|
|
37
|
+
verification: [
|
|
38
|
+
'gsd-verifier',
|
|
39
|
+
'gsd-integration-checker'
|
|
40
|
+
]
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Load profile configuration from .planning/config.json
|
|
45
|
+
*
|
|
46
|
+
* @param {string} cwd - Current working directory
|
|
47
|
+
* @returns {Object|null} Parsed config object or null on error
|
|
48
|
+
*/
|
|
49
|
+
function loadProfileConfig(cwd) {
|
|
50
|
+
try {
|
|
51
|
+
const configPath = path.join(cwd, '.planning', 'config.json');
|
|
52
|
+
|
|
53
|
+
if (!fs.existsSync(configPath)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
58
|
+
let config = JSON.parse(content);
|
|
59
|
+
|
|
60
|
+
// Auto-migrate old key name: current_os_profile → current_oc_profile
|
|
61
|
+
if (config.current_os_profile && !config.current_oc_profile) {
|
|
62
|
+
config.current_oc_profile = config.current_os_profile;
|
|
63
|
+
delete config.current_os_profile;
|
|
64
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return config;
|
|
68
|
+
} catch (err) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Apply profile configuration to opencode.json
|
|
75
|
+
* Updates agent model assignments based on profile
|
|
76
|
+
*
|
|
77
|
+
* @param {string} opencodePath - Path to opencode.json
|
|
78
|
+
* @param {string} configPath - Path to .planning/config.json
|
|
79
|
+
* @param {string} [profileName] - Optional profile name to use (overrides current_oc_profile)
|
|
80
|
+
* @returns {Object} {success: true, updated: [agentNames]} or {success: false, error: {code, message}}
|
|
81
|
+
*/
|
|
82
|
+
function applyProfileToOpencode(opencodePath, configPath, profileName = null) {
|
|
83
|
+
try {
|
|
84
|
+
// Load profile config
|
|
85
|
+
let config;
|
|
86
|
+
if (fs.existsSync(configPath)) {
|
|
87
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
88
|
+
config = JSON.parse(content);
|
|
89
|
+
} else {
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
error: {
|
|
93
|
+
code: 'CONFIG_NOT_FOUND',
|
|
94
|
+
message: `.planning/config.json not found at ${configPath}`
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Determine which profile to use
|
|
100
|
+
const targetProfile = profileName || config.current_oc_profile;
|
|
101
|
+
|
|
102
|
+
if (!targetProfile) {
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
error: {
|
|
106
|
+
code: 'PROFILE_NOT_FOUND',
|
|
107
|
+
message: 'current_oc_profile not found in config.json. Run set-profile with a profile name first.'
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Validate profile exists in profiles.presets
|
|
113
|
+
const presets = config.profiles?.presets;
|
|
114
|
+
if (!presets || !presets[targetProfile]) {
|
|
115
|
+
const availableProfiles = presets ? Object.keys(presets).join(', ') : 'none';
|
|
116
|
+
return {
|
|
117
|
+
success: false,
|
|
118
|
+
error: {
|
|
119
|
+
code: 'PROFILE_NOT_FOUND',
|
|
120
|
+
message: `Profile "${targetProfile}" not found in profiles.presets. Available profiles: ${availableProfiles}`
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Load or create opencode.json
|
|
126
|
+
let opencodeData;
|
|
127
|
+
if (!fs.existsSync(opencodePath)) {
|
|
128
|
+
// Create initial opencode.json structure
|
|
129
|
+
opencodeData = {
|
|
130
|
+
"$schema": "https://opencode.ai/config.json",
|
|
131
|
+
"agent": {}
|
|
132
|
+
};
|
|
133
|
+
} else {
|
|
134
|
+
// Load existing opencode.json
|
|
135
|
+
const opencodeContent = fs.readFileSync(opencodePath, 'utf8');
|
|
136
|
+
opencodeData = JSON.parse(opencodeContent);
|
|
137
|
+
|
|
138
|
+
// Ensure agent object exists
|
|
139
|
+
if (!opencodeData.agent) {
|
|
140
|
+
opencodeData.agent = {};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Get model assignments from profiles.presets.{profile_name}.models
|
|
145
|
+
const profileModels = presets[targetProfile];
|
|
146
|
+
|
|
147
|
+
if (!profileModels.planning && !profileModels.execution && !profileModels.verification) {
|
|
148
|
+
return {
|
|
149
|
+
success: false,
|
|
150
|
+
error: {
|
|
151
|
+
code: 'PROFILE_NOT_FOUND',
|
|
152
|
+
message: `No model assignments found for profile "${targetProfile}"`
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Apply model assignments to agents (MERGE - preserve non-gsd agents)
|
|
158
|
+
const updatedAgents = [];
|
|
159
|
+
|
|
160
|
+
// Initialize agent object if it doesn't exist
|
|
161
|
+
if (!opencodeData.agent) {
|
|
162
|
+
opencodeData.agent = {};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Apply each profile category - ONLY update gsd-* agents
|
|
166
|
+
for (const [category, agentNames] of Object.entries(PROFILE_AGENT_MAPPING)) {
|
|
167
|
+
const modelId = profileModels[category];
|
|
168
|
+
|
|
169
|
+
if (modelId) {
|
|
170
|
+
for (const agentName of agentNames) {
|
|
171
|
+
// Only update gsd-* agents, preserve all others
|
|
172
|
+
if (typeof opencodeData.agent[agentName] === 'object' && opencodeData.agent[agentName] !== null) {
|
|
173
|
+
opencodeData.agent[agentName].model = modelId;
|
|
174
|
+
} else {
|
|
175
|
+
opencodeData.agent[agentName] = { model: modelId };
|
|
176
|
+
}
|
|
177
|
+
updatedAgents.push({ agent: agentName, model: modelId });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// write updated opencode.json
|
|
183
|
+
fs.writeFileSync(opencodePath, JSON.stringify(opencodeData, null, 2) + '\n', 'utf8');
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
success: true,
|
|
187
|
+
updated: updatedAgents
|
|
188
|
+
};
|
|
189
|
+
} catch (err) {
|
|
190
|
+
return {
|
|
191
|
+
success: false,
|
|
192
|
+
error: {
|
|
193
|
+
code: 'UPDATE_FAILED',
|
|
194
|
+
message: `Failed to apply profile: ${err.message}`
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = {
|
|
201
|
+
loadProfileConfig,
|
|
202
|
+
applyProfileToOpencode,
|
|
203
|
+
VALID_PROFILES,
|
|
204
|
+
PROFILE_AGENT_MAPPING
|
|
205
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* oc-core.cjs — Shared utilities for gsd-oc-tools CLI
|
|
3
|
+
*
|
|
4
|
+
* Provides common functions for output formatting, error handling, file operations.
|
|
5
|
+
* Follows gsd-tools.cjs architecture pattern.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Output result in JSON envelope format
|
|
13
|
+
* Large payloads (>50KB) are written to temp file with @file: prefix
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} result - The result data to output
|
|
16
|
+
* @param {boolean} raw - If true, output raw value instead of envelope
|
|
17
|
+
* @param {*} rawValue - The raw value to output if raw=true
|
|
18
|
+
*/
|
|
19
|
+
function output(result, raw = false, rawValue = null) {
|
|
20
|
+
let outputStr;
|
|
21
|
+
|
|
22
|
+
if (raw && rawValue !== null) {
|
|
23
|
+
// rawValue is already stringified, use it directly
|
|
24
|
+
outputStr = rawValue;
|
|
25
|
+
} else {
|
|
26
|
+
outputStr = JSON.stringify(result, null, 2);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Large payload handling (>50KB)
|
|
30
|
+
if (outputStr.length > 50 * 1024) {
|
|
31
|
+
const tempFile = path.join(require('os').tmpdir(), `gsd-oc-${Date.now()}.json`);
|
|
32
|
+
fs.writeFileSync(tempFile, outputStr, 'utf8');
|
|
33
|
+
console.log(`@file:${tempFile}`);
|
|
34
|
+
} else {
|
|
35
|
+
console.log(outputStr);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Output error in standardized envelope format to stderr
|
|
41
|
+
*
|
|
42
|
+
* @param {string} message - Error message
|
|
43
|
+
* @param {string} code - Error code (e.g., 'CONFIG_NOT_FOUND', 'INVALID_JSON')
|
|
44
|
+
*/
|
|
45
|
+
function error(message, code = 'UNKNOWN_ERROR') {
|
|
46
|
+
const errorEnvelope = {
|
|
47
|
+
success: false,
|
|
48
|
+
error: {
|
|
49
|
+
code,
|
|
50
|
+
message
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
console.error(JSON.stringify(errorEnvelope, null, 2));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Safely read a file, returning null on failure
|
|
59
|
+
*
|
|
60
|
+
* @param {string} filePath - Path to file
|
|
61
|
+
* @returns {string|null} File contents or null
|
|
62
|
+
*/
|
|
63
|
+
function safeReadFile(filePath) {
|
|
64
|
+
try {
|
|
65
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
66
|
+
} catch (err) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create timestamped backup of a file
|
|
73
|
+
*
|
|
74
|
+
* @param {string} filePath - Path to file to backup
|
|
75
|
+
* @param {string} backupDir - Directory for backups (.opencode-backups/)
|
|
76
|
+
* @returns {string|null} Backup file path or null on failure
|
|
77
|
+
*/
|
|
78
|
+
function createBackup(filePath, backupDir = '.opencode-backups') {
|
|
79
|
+
try {
|
|
80
|
+
// Ensure backup directory exists
|
|
81
|
+
if (!fs.existsSync(backupDir)) {
|
|
82
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// read original file
|
|
86
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
87
|
+
|
|
88
|
+
// Create timestamped filename (YYYYMMDD-HHmmss-SSS format)
|
|
89
|
+
const now = new Date();
|
|
90
|
+
const timestamp = now.toISOString()
|
|
91
|
+
.replace(/[-:T]/g, '')
|
|
92
|
+
.replace(/\.\d{3}Z$/, '')
|
|
93
|
+
.replace(/(\d{8})(\d{6})(\d{3})/, '$1-$2-$3');
|
|
94
|
+
|
|
95
|
+
const fileName = path.basename(filePath);
|
|
96
|
+
const backupFileName = `${timestamp}-${fileName}`;
|
|
97
|
+
const backupPath = path.join(backupDir, backupFileName);
|
|
98
|
+
|
|
99
|
+
// write backup
|
|
100
|
+
fs.writeFileSync(backupPath, content, 'utf8');
|
|
101
|
+
|
|
102
|
+
return backupPath;
|
|
103
|
+
} catch (err) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
output,
|
|
110
|
+
error,
|
|
111
|
+
safeReadFile,
|
|
112
|
+
createBackup
|
|
113
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* oc-models.cjs — Model catalog operations for gsd-oc-tools CLI
|
|
3
|
+
*
|
|
4
|
+
* Provides functions for fetching and validating model IDs against opencode models output.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { execSync } = require('child_process');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Fetch model catalog from opencode models command
|
|
13
|
+
*
|
|
14
|
+
* @returns {Object} {success: boolean, models: string[]} or {success: false, error: {...}}
|
|
15
|
+
*/
|
|
16
|
+
function getModelCatalog() {
|
|
17
|
+
try {
|
|
18
|
+
const output = execSync('opencode models', {
|
|
19
|
+
encoding: 'utf8',
|
|
20
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Parse output (one model per line)
|
|
24
|
+
const models = output
|
|
25
|
+
.split('\n')
|
|
26
|
+
.map(line => line.trim())
|
|
27
|
+
.filter(line => line.length > 0);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
success: true,
|
|
31
|
+
models
|
|
32
|
+
};
|
|
33
|
+
} catch (err) {
|
|
34
|
+
return {
|
|
35
|
+
success: false,
|
|
36
|
+
error: {
|
|
37
|
+
code: 'FETCH_FAILED',
|
|
38
|
+
message: `Failed to fetch model catalog: ${err.message}`
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validate model IDs in opencode.json against valid models list
|
|
46
|
+
*
|
|
47
|
+
* @param {string} opencodePath - Path to opencode.json file
|
|
48
|
+
* @param {string[]} validModels - Array of valid model IDs
|
|
49
|
+
* @returns {Object} {valid, total, validCount, invalidCount, issues: [{agent, model, reason}]}
|
|
50
|
+
*/
|
|
51
|
+
function validateModelIds(opencodePath, validModels) {
|
|
52
|
+
const issues = [];
|
|
53
|
+
let total = 0;
|
|
54
|
+
let validCount = 0;
|
|
55
|
+
let invalidCount = 0;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const content = fs.readFileSync(opencodePath, 'utf8');
|
|
59
|
+
const opencodeData = JSON.parse(content);
|
|
60
|
+
|
|
61
|
+
// Look for agent model assignments
|
|
62
|
+
// Common patterns: agent.model, profiles.*.model, models.*
|
|
63
|
+
const assignments = [];
|
|
64
|
+
|
|
65
|
+
// Check for agents at root level
|
|
66
|
+
if (opencodeData.agent && typeof opencodeData.agent === 'object') {
|
|
67
|
+
Object.entries(opencodeData.agent).forEach(([agentName, config]) => {
|
|
68
|
+
if (typeof config === 'string') {
|
|
69
|
+
assignments.push({ agent: `agent.${agentName}`, model: config });
|
|
70
|
+
} else if (config && typeof config === 'object' && config.model) {
|
|
71
|
+
assignments.push({ agent: `agent.${agentName}`, model: config.model });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check for profiles
|
|
77
|
+
if (opencodeData.profiles && typeof opencodeData.profiles === 'object') {
|
|
78
|
+
Object.entries(opencodeData.profiles).forEach(([profileName, config]) => {
|
|
79
|
+
if (config && typeof config === 'object') {
|
|
80
|
+
Object.entries(config).forEach(([key, value]) => {
|
|
81
|
+
if (key.includes('model') && typeof value === 'string') {
|
|
82
|
+
assignments.push({ agent: `profiles.${profileName}.${key}`, model: value });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check for models at root level
|
|
90
|
+
if (opencodeData.models && typeof opencodeData.models === 'object') {
|
|
91
|
+
Object.entries(opencodeData.models).forEach(([modelName, modelId]) => {
|
|
92
|
+
if (typeof modelId === 'string') {
|
|
93
|
+
assignments.push({ agent: `models.${modelName}`, model: modelId });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Validate each assignment
|
|
99
|
+
total = assignments.length;
|
|
100
|
+
for (const { agent, model } of assignments) {
|
|
101
|
+
if (validModels.includes(model)) {
|
|
102
|
+
validCount++;
|
|
103
|
+
} else {
|
|
104
|
+
invalidCount++;
|
|
105
|
+
issues.push({
|
|
106
|
+
agent,
|
|
107
|
+
model,
|
|
108
|
+
reason: 'Model ID not found in opencode models catalog'
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
valid: invalidCount === 0,
|
|
115
|
+
total,
|
|
116
|
+
validCount,
|
|
117
|
+
invalidCount,
|
|
118
|
+
issues
|
|
119
|
+
};
|
|
120
|
+
} catch (err) {
|
|
121
|
+
if (err.code === 'ENOENT') {
|
|
122
|
+
throw new Error('CONFIG_NOT_FOUND');
|
|
123
|
+
} else if (err instanceof SyntaxError) {
|
|
124
|
+
throw new Error('INVALID_JSON');
|
|
125
|
+
}
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
getModelCatalog,
|
|
132
|
+
validateModelIds
|
|
133
|
+
};
|