@aexol/opencode-wizard 0.1.0 → 0.1.3
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/README.md +26 -7
- package/dist/server.d.ts +108 -5
- package/dist/server.js +1561 -1401
- package/dist/server.js.map +1 -0
- package/dist/smoke-published-skills.js +80 -75
- package/dist/smoke-published-skills.js.map +1 -0
- package/dist/tui.d.ts +60 -0
- package/dist/tui.js +288 -0
- package/dist/tui.js.map +1 -0
- package/package.json +17 -6
package/dist/server.js
CHANGED
|
@@ -5,14 +5,13 @@ import crypto from 'node:crypto';
|
|
|
5
5
|
import { execFile } from 'node:child_process';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
7
|
import { URL, fileURLToPath } from 'node:url';
|
|
8
|
-
import { tool } from '@opencode-ai/plugin';
|
|
9
8
|
const execFileAsync = promisify(execFile);
|
|
10
9
|
const MODULE_FILE_PATH = fileURLToPath(import.meta.url);
|
|
11
10
|
const PACKAGE_ROOT_PATH = path.resolve(path.dirname(MODULE_FILE_PATH), '..');
|
|
12
|
-
const PLUGIN_ID = 'opencode-wizard
|
|
11
|
+
export const PLUGIN_ID = 'opencode-wizard';
|
|
13
12
|
const CACHE_TTL_MS = 30_000;
|
|
14
13
|
const ROOT_SKILL_SEED_PATH = '.opencode/skills';
|
|
15
|
-
const AUTH_STATE_PATH = 'plugin/opencode-wizard
|
|
14
|
+
const AUTH_STATE_PATH = 'plugin/opencode-wizard/.generated/auth-state.json';
|
|
16
15
|
const LOCAL_DEV_BACKEND_ORIGIN = 'http://localhost:8080';
|
|
17
16
|
const PUBLISHED_BACKEND_ORIGIN = 'https://opencode-wizard.aexol.work';
|
|
18
17
|
const OIDC_ISSUER = 'https://login.microsoftonline.com/86f4caf4-0d6f-4682-9a06-ea57f3e4e76c/v2.0';
|
|
@@ -22,28 +21,38 @@ const OIDC_CALLBACK_PATH = '/oauth/callback';
|
|
|
22
21
|
const OIDC_CALLBACK_URL = `${OIDC_CALLBACK_ORIGIN}${OIDC_CALLBACK_PATH}`;
|
|
23
22
|
const OIDC_SCOPES = ['openid', 'profile', 'email'];
|
|
24
23
|
const LOGIN_TIMEOUT_MS = 5 * 60_000;
|
|
24
|
+
const SYSTEM_NOTE_SKILL_NAME_LIMIT = 10;
|
|
25
|
+
const SYSTEM_NOTE_DETAIL_LIMIT = 3;
|
|
26
|
+
const SYSTEM_NOTE_DETAIL_CHAR_LIMIT = 2_400;
|
|
27
|
+
const SYSTEM_NOTE_SKILL_DESCRIPTION_LIMIT = 140;
|
|
25
28
|
const PRESENCE_EVENT_TIMEOUT_MS = 3_000;
|
|
26
29
|
const PRESENCE_EVENT_MAX_ATTEMPTS = 2;
|
|
27
30
|
const PRESENCE_EVENT_RETRY_DELAY_MS = 250;
|
|
28
31
|
const PRESENCE_SHUTDOWN_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
29
32
|
const PRESENCE_SIGNAL_EXIT_CODES = {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
SIGINT: 130,
|
|
34
|
+
SIGTERM: 143,
|
|
35
|
+
SIGHUP: 129
|
|
33
36
|
};
|
|
34
37
|
const createIdleLoginBootstrapSnapshot = () => ({
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
status: 'idle',
|
|
39
|
+
trigger: null,
|
|
40
|
+
startedAt: null,
|
|
41
|
+
expiresAt: null,
|
|
42
|
+
browserUrl: null,
|
|
43
|
+
browserOpenError: null,
|
|
44
|
+
email: null,
|
|
45
|
+
message: null
|
|
43
46
|
});
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
const importOpencodePluginModule = new Function('specifier', 'return import(specifier)');
|
|
48
|
+
export const AVAILABLE_PUBLISHED_SKILL_TOOLS = ['opencode_wizard_published_skills_fetch'];
|
|
49
|
+
export const NATIVE_SKILLS_URL_COMPATIBILITY = {
|
|
50
|
+
configKey: 'skills.urls',
|
|
51
|
+
deliveryMode: 'public_static_registry',
|
|
52
|
+
wizardPrivateDelivery: 'authenticated_scoped_fetch_tool',
|
|
53
|
+
authSupport: 'none',
|
|
54
|
+
guidance: 'OpenCode skills.urls is for public/static registries and complements opencode-wizard; private workspace-scoped skills stay available through the authenticated fetch tool only.'
|
|
55
|
+
};
|
|
47
56
|
const PUBLISHED_SKILLS_CATALOG_QUERY = `
|
|
48
57
|
query PluginPublishedSkills($input: PublishedSkillsDeliveryInput!) {
|
|
49
58
|
pluginPublishedSkills(input: $input) {
|
|
@@ -66,7 +75,9 @@ const PUBLISHED_SKILLS_CATALOG_QUERY = `
|
|
|
66
75
|
slug
|
|
67
76
|
name
|
|
68
77
|
summary
|
|
78
|
+
whenToUse
|
|
69
79
|
status
|
|
80
|
+
installPolicy
|
|
70
81
|
tags {
|
|
71
82
|
id
|
|
72
83
|
slug
|
|
@@ -111,348 +122,347 @@ const PUBLISHED_SKILL_DETAIL_QUERY = `
|
|
|
111
122
|
}
|
|
112
123
|
}
|
|
113
124
|
`;
|
|
114
|
-
const parseDotEnvValue =
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return trimmedValue;
|
|
121
|
-
};
|
|
122
|
-
const parseDotEnv = (raw) => {
|
|
123
|
-
const values = new Map();
|
|
124
|
-
for (const line of raw.split(/\r?\n/u)) {
|
|
125
|
-
const trimmedLine = line.trim();
|
|
126
|
-
if (!trimmedLine || trimmedLine.startsWith('#'))
|
|
127
|
-
continue;
|
|
128
|
-
const separatorIndex = trimmedLine.indexOf('=');
|
|
129
|
-
if (separatorIndex <= 0)
|
|
130
|
-
continue;
|
|
131
|
-
const key = trimmedLine.slice(0, separatorIndex).trim();
|
|
132
|
-
if (!key)
|
|
133
|
-
continue;
|
|
134
|
-
const rawValue = trimmedLine.slice(separatorIndex + 1);
|
|
135
|
-
values.set(key, parseDotEnvValue(rawValue));
|
|
136
|
-
}
|
|
137
|
-
return values;
|
|
125
|
+
const parseDotEnvValue = value => {
|
|
126
|
+
const trimmedValue = value.trim();
|
|
127
|
+
if (trimmedValue.startsWith('"') && trimmedValue.endsWith('"') || trimmedValue.startsWith("'") && trimmedValue.endsWith("'")) {
|
|
128
|
+
return trimmedValue.slice(1, -1);
|
|
129
|
+
}
|
|
130
|
+
return trimmedValue;
|
|
138
131
|
};
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
}
|
|
132
|
+
const parseDotEnv = raw => {
|
|
133
|
+
const values = new Map();
|
|
134
|
+
for (const line of raw.split(/\r?\n/u)) {
|
|
135
|
+
const trimmedLine = line.trim();
|
|
136
|
+
if (!trimmedLine || trimmedLine.startsWith('#')) continue;
|
|
137
|
+
const separatorIndex = trimmedLine.indexOf('=');
|
|
138
|
+
if (separatorIndex <= 0) continue;
|
|
139
|
+
const key = trimmedLine.slice(0, separatorIndex).trim();
|
|
140
|
+
if (!key) continue;
|
|
141
|
+
const rawValue = trimmedLine.slice(separatorIndex + 1);
|
|
142
|
+
values.set(key, parseDotEnvValue(rawValue));
|
|
143
|
+
}
|
|
144
|
+
return values;
|
|
154
145
|
};
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
146
|
+
const findUpwardFile = async (startDirectory, fileName) => {
|
|
147
|
+
let currentDirectory = normalizeAbsolutePath(startDirectory);
|
|
148
|
+
while (true) {
|
|
149
|
+
const candidatePath = path.join(currentDirectory, fileName);
|
|
159
150
|
try {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
151
|
+
await fs.access(candidatePath);
|
|
152
|
+
return candidatePath;
|
|
153
|
+
} catch {
|
|
154
|
+
const parentDirectory = path.dirname(currentDirectory);
|
|
155
|
+
if (parentDirectory === currentDirectory) return null;
|
|
156
|
+
currentDirectory = parentDirectory;
|
|
165
157
|
}
|
|
158
|
+
}
|
|
166
159
|
};
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
catch {
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return false;
|
|
160
|
+
const readLocalEnvValues = async startDirectory => {
|
|
161
|
+
const envPath = await findUpwardFile(startDirectory, '.env');
|
|
162
|
+
if (!envPath) return new Map();
|
|
163
|
+
try {
|
|
164
|
+
const raw = await fs.readFile(envPath, 'utf8');
|
|
165
|
+
return parseDotEnv(raw);
|
|
166
|
+
} catch {
|
|
167
|
+
return new Map();
|
|
168
|
+
}
|
|
182
169
|
};
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
170
|
+
const isLocalModulePathExecution = async () => {
|
|
171
|
+
const localDevSentinelPaths = [path.join(PACKAGE_ROOT_PATH, 'src', 'server.ts'), path.join(PACKAGE_ROOT_PATH, 'tsconfig.json')];
|
|
172
|
+
for (const sentinelPath of localDevSentinelPaths) {
|
|
186
173
|
try {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return null;
|
|
174
|
+
await fs.access(sentinelPath);
|
|
175
|
+
return true;
|
|
176
|
+
} catch {
|
|
177
|
+
continue;
|
|
192
178
|
}
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
193
181
|
};
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (configuredAppUrl)
|
|
203
|
-
return configuredAppUrl;
|
|
204
|
-
if (await isLocalModulePathExecution())
|
|
205
|
-
return LOCAL_DEV_BACKEND_ORIGIN;
|
|
206
|
-
return PUBLISHED_BACKEND_ORIGIN;
|
|
207
|
-
};
|
|
208
|
-
const toWorkspaceSlug = (value) => {
|
|
209
|
-
const normalized = value
|
|
210
|
-
.trim()
|
|
211
|
-
.toLowerCase()
|
|
212
|
-
.replace(/[^a-z0-9-]+/gu, '-')
|
|
213
|
-
.replace(/^-+|-+$/gu, '');
|
|
214
|
-
if (normalized)
|
|
215
|
-
return normalized;
|
|
216
|
-
return 'workspace';
|
|
217
|
-
};
|
|
218
|
-
const resolveFallbackWorkspaceSlug = (worktree) => {
|
|
219
|
-
const configuredWorkspaceSlug = process.env.OPENCODE_WIZARD_SKILLS_WORKSPACE_SLUG?.trim();
|
|
220
|
-
if (configuredWorkspaceSlug)
|
|
221
|
-
return toWorkspaceSlug(configuredWorkspaceSlug);
|
|
222
|
-
return toWorkspaceSlug(path.basename(path.resolve(worktree)));
|
|
223
|
-
};
|
|
224
|
-
export const resolveConfig = async (worktree) => {
|
|
225
|
-
const backendOrigin = await resolveBackendOrigin(worktree);
|
|
226
|
-
return {
|
|
227
|
-
backendOrigin,
|
|
228
|
-
graphqlUrl: `${backendOrigin}/graphql`,
|
|
229
|
-
authSessionUrl: `${backendOrigin}/api/opencode-plugin/auth/session`,
|
|
230
|
-
presenceUrl: `${backendOrigin}/api/opencode-plugin/presence`,
|
|
231
|
-
actionsUrl: `${backendOrigin}/api/opencode-plugin/actions`,
|
|
232
|
-
fallbackWorkspaceSlug: resolveFallbackWorkspaceSlug(worktree),
|
|
233
|
-
rootSkillSeedPath: ROOT_SKILL_SEED_PATH,
|
|
234
|
-
authStatePath: AUTH_STATE_PATH,
|
|
235
|
-
};
|
|
182
|
+
const normalizeBackendOrigin = value => {
|
|
183
|
+
if (!value) return null;
|
|
184
|
+
try {
|
|
185
|
+
const normalizedUrl = new URL(value);
|
|
186
|
+
return normalizedUrl.origin;
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
236
190
|
};
|
|
237
|
-
const
|
|
191
|
+
const resolveBackendOrigin = async worktree => {
|
|
192
|
+
const envValues = await readLocalEnvValues(worktree);
|
|
193
|
+
const configuredPort = envValues.get('PORT') ?? process.env.PORT;
|
|
194
|
+
const normalizedPort = configuredPort ? Number.parseInt(configuredPort, 10) : Number.NaN;
|
|
195
|
+
if (Number.isInteger(normalizedPort) && normalizedPort >= 1 && normalizedPort <= 65535) {
|
|
196
|
+
return `http://localhost:${normalizedPort}`;
|
|
197
|
+
}
|
|
198
|
+
const configuredAppUrl = normalizeBackendOrigin(envValues.get('APP_URL')) ?? normalizeBackendOrigin(process.env.APP_URL);
|
|
199
|
+
if (configuredAppUrl) return configuredAppUrl;
|
|
200
|
+
if (await isLocalModulePathExecution()) return LOCAL_DEV_BACKEND_ORIGIN;
|
|
201
|
+
return PUBLISHED_BACKEND_ORIGIN;
|
|
202
|
+
};
|
|
203
|
+
const toWorkspaceSlug = value => {
|
|
204
|
+
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9-]+/gu, '-').replace(/^-+|-+$/gu, '');
|
|
205
|
+
if (normalized) return normalized;
|
|
206
|
+
return 'workspace';
|
|
207
|
+
};
|
|
208
|
+
const resolveFallbackWorkspaceSlug = worktree => {
|
|
209
|
+
const configuredWorkspaceSlug = process.env.OPENCODE_WIZARD_SKILLS_WORKSPACE_SLUG?.trim();
|
|
210
|
+
if (configuredWorkspaceSlug) return toWorkspaceSlug(configuredWorkspaceSlug);
|
|
211
|
+
return toWorkspaceSlug(path.basename(path.resolve(worktree)));
|
|
212
|
+
};
|
|
213
|
+
export const resolveConfig = async worktree => {
|
|
214
|
+
const backendOrigin = await resolveBackendOrigin(worktree);
|
|
215
|
+
return {
|
|
216
|
+
backendOrigin,
|
|
217
|
+
graphqlUrl: `${backendOrigin}/graphql`,
|
|
218
|
+
authSessionUrl: `${backendOrigin}/api/opencode-plugin/oauth/session`,
|
|
219
|
+
presenceUrl: `${backendOrigin}/api/opencode-plugin/presence`,
|
|
220
|
+
actionsUrl: `${backendOrigin}/api/opencode-plugin/actions`,
|
|
221
|
+
fallbackWorkspaceSlug: resolveFallbackWorkspaceSlug(worktree),
|
|
222
|
+
rootSkillSeedPath: ROOT_SKILL_SEED_PATH,
|
|
223
|
+
authStatePath: AUTH_STATE_PATH
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
const normalizeAbsolutePath = value => path.resolve(value);
|
|
238
227
|
const normalizeRepositoryPath = (worktree, directory) => {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
return '.';
|
|
246
|
-
return relativePath.split(path.sep).join('/');
|
|
247
|
-
};
|
|
248
|
-
const tryExecGit = async (args) => {
|
|
249
|
-
try {
|
|
250
|
-
const { stdout } = await execFileAsync('git', args, {
|
|
251
|
-
encoding: 'utf8',
|
|
252
|
-
});
|
|
253
|
-
const normalizedOutput = stdout.trim();
|
|
254
|
-
if (normalizedOutput)
|
|
255
|
-
return normalizedOutput;
|
|
256
|
-
return null;
|
|
257
|
-
}
|
|
258
|
-
catch {
|
|
259
|
-
return null;
|
|
260
|
-
}
|
|
228
|
+
const absoluteWorktree = normalizeAbsolutePath(worktree);
|
|
229
|
+
const absoluteDirectory = normalizeAbsolutePath(directory);
|
|
230
|
+
const relativePath = path.relative(absoluteWorktree, absoluteDirectory);
|
|
231
|
+
if (!relativePath || relativePath === '') return '.';
|
|
232
|
+
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) return '.';
|
|
233
|
+
return relativePath.split(path.sep).join('/');
|
|
261
234
|
};
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
235
|
+
const tryExecGit = async args => {
|
|
236
|
+
try {
|
|
237
|
+
const {
|
|
238
|
+
stdout
|
|
239
|
+
} = await execFileAsync('git', args, {
|
|
240
|
+
encoding: 'utf8'
|
|
241
|
+
});
|
|
242
|
+
const normalizedOutput = stdout.trim();
|
|
243
|
+
if (normalizedOutput) return normalizedOutput;
|
|
244
|
+
return null;
|
|
245
|
+
} catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
const resolveGitRoot = async directory => {
|
|
250
|
+
const gitRoot = await tryExecGit(['-C', directory, 'rev-parse', '--show-toplevel']);
|
|
251
|
+
if (!gitRoot) return null;
|
|
252
|
+
return normalizeAbsolutePath(gitRoot);
|
|
253
|
+
};
|
|
254
|
+
const normalizeGitRemoteUrl = remoteUrl => {
|
|
255
|
+
if (!remoteUrl) return null;
|
|
256
|
+
const trimmedRemoteUrl = remoteUrl.trim();
|
|
257
|
+
if (!trimmedRemoteUrl) return null;
|
|
258
|
+
const scpLikeMatch = /^git@([^:]+):(.+)$/u.exec(trimmedRemoteUrl);
|
|
259
|
+
if (scpLikeMatch) {
|
|
260
|
+
return `ssh://git@${scpLikeMatch[1]}/${scpLikeMatch[2].replace(/^\/+/, '')}`;
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const parsedUrl = new URL(trimmedRemoteUrl);
|
|
264
|
+
return parsedUrl.toString().replace(/\/+$/u, '');
|
|
265
|
+
} catch {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
const resolveGitRemoteOriginUrl = async repositoryRoot => {
|
|
270
|
+
const remoteUrl = await tryExecGit(['-C', repositoryRoot, 'remote', 'get-url', 'origin']);
|
|
271
|
+
return normalizeGitRemoteUrl(remoteUrl);
|
|
272
|
+
};
|
|
273
|
+
const resolveWorkspace = async ({
|
|
274
|
+
config,
|
|
275
|
+
directory
|
|
276
|
+
}) => {
|
|
277
|
+
const requestedDirectory = normalizeAbsolutePath(directory);
|
|
278
|
+
const gitRoot = await resolveGitRoot(requestedDirectory);
|
|
279
|
+
const repositoryRoot = gitRoot ?? requestedDirectory;
|
|
280
|
+
const repositoryUrl = gitRoot ? await resolveGitRemoteOriginUrl(gitRoot) : null;
|
|
281
|
+
const fallbackWorkspaceSlug = config.fallbackWorkspaceSlug;
|
|
282
|
+
const directoryPath = normalizeRepositoryPath(repositoryRoot, requestedDirectory);
|
|
283
|
+
const workspaceIdentity = `workspaceSlug:${fallbackWorkspaceSlug}`;
|
|
284
|
+
return {
|
|
285
|
+
requestedDirectory,
|
|
286
|
+
repositoryRoot,
|
|
287
|
+
repositoryUrl,
|
|
288
|
+
fallbackWorkspaceSlug,
|
|
289
|
+
directoryPath,
|
|
290
|
+
cacheKey: JSON.stringify([workspaceIdentity, directoryPath])
|
|
291
|
+
};
|
|
285
292
|
};
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
return normalizeGitRemoteUrl(remoteUrl);
|
|
289
|
-
};
|
|
290
|
-
const resolveWorkspace = async ({ config, directory, }) => {
|
|
291
|
-
const requestedDirectory = normalizeAbsolutePath(directory);
|
|
292
|
-
const gitRoot = await resolveGitRoot(requestedDirectory);
|
|
293
|
-
const repositoryRoot = gitRoot ?? requestedDirectory;
|
|
294
|
-
const repositoryUrl = gitRoot ? await resolveGitRemoteOriginUrl(gitRoot) : null;
|
|
295
|
-
const fallbackWorkspaceSlug = config.fallbackWorkspaceSlug;
|
|
296
|
-
const directoryPath = normalizeRepositoryPath(repositoryRoot, requestedDirectory);
|
|
297
|
-
const workspaceIdentity = `workspaceSlug:${fallbackWorkspaceSlug}`;
|
|
293
|
+
const toDeliveryInput = resolution => {
|
|
294
|
+
if (resolution.fallbackWorkspaceSlug) {
|
|
298
295
|
return {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
repositoryUrl,
|
|
302
|
-
fallbackWorkspaceSlug,
|
|
303
|
-
directoryPath,
|
|
304
|
-
cacheKey: JSON.stringify([workspaceIdentity, directoryPath]),
|
|
296
|
+
workspaceSlug: resolution.fallbackWorkspaceSlug,
|
|
297
|
+
directoryPath: resolution.directoryPath
|
|
305
298
|
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (resolution.fallbackWorkspaceSlug) {
|
|
309
|
-
return {
|
|
310
|
-
workspaceSlug: resolution.fallbackWorkspaceSlug,
|
|
311
|
-
directoryPath: resolution.directoryPath,
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
if (resolution.repositoryUrl) {
|
|
315
|
-
return {
|
|
316
|
-
repositoryUrl: resolution.repositoryUrl,
|
|
317
|
-
directoryPath: resolution.directoryPath,
|
|
318
|
-
};
|
|
319
|
-
}
|
|
299
|
+
}
|
|
300
|
+
if (resolution.repositoryUrl) {
|
|
320
301
|
return {
|
|
321
|
-
|
|
322
|
-
|
|
302
|
+
repositoryUrl: resolution.repositoryUrl,
|
|
303
|
+
directoryPath: resolution.directoryPath
|
|
323
304
|
};
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
workspaceSlug: 'workspace',
|
|
308
|
+
directoryPath: resolution.directoryPath
|
|
309
|
+
};
|
|
324
310
|
};
|
|
325
|
-
const formatSkillLabel =
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
return item.skill.name;
|
|
311
|
+
const formatSkillLabel = item => {
|
|
312
|
+
const artifactName = item.publishedArtifact.frontmatterName.trim();
|
|
313
|
+
if (artifactName.length > 0) return artifactName;
|
|
314
|
+
return item.skill.name;
|
|
330
315
|
};
|
|
331
|
-
const toFrontmatterString =
|
|
332
|
-
const readJsonFile = async
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
316
|
+
const toFrontmatterString = value => JSON.stringify(value);
|
|
317
|
+
const readJsonFile = async filePath => {
|
|
318
|
+
try {
|
|
319
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
320
|
+
const parsed = JSON.parse(raw);
|
|
321
|
+
return parsed;
|
|
322
|
+
} catch {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
341
325
|
};
|
|
342
326
|
const writeJsonFile = async (filePath, value) => {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if (isAuthState(storedAuthState))
|
|
370
|
-
return storedAuthState;
|
|
371
|
-
await deleteFileIfExists(authStateFile);
|
|
372
|
-
return null;
|
|
327
|
+
await fs.mkdir(path.dirname(filePath), {
|
|
328
|
+
recursive: true
|
|
329
|
+
});
|
|
330
|
+
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
331
|
+
};
|
|
332
|
+
const deleteFileIfExists = async filePath => {
|
|
333
|
+
await fs.rm(filePath, {
|
|
334
|
+
force: true
|
|
335
|
+
});
|
|
336
|
+
};
|
|
337
|
+
const isRecord = value => {
|
|
338
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
339
|
+
};
|
|
340
|
+
const isValidIsoDateString = value => {
|
|
341
|
+
return typeof value === 'string' && Number.isFinite(Date.parse(value));
|
|
342
|
+
};
|
|
343
|
+
const isAuthState = value => {
|
|
344
|
+
if (!isRecord(value)) return false;
|
|
345
|
+
return value.pluginId === PLUGIN_ID && typeof value.sessionToken === 'string' && isValidIsoDateString(value.expiresAt) && isValidIsoDateString(value.authenticatedAt) && typeof value.userId === 'string' && typeof value.email === 'string';
|
|
346
|
+
};
|
|
347
|
+
const readAuthState = async authStateFile => {
|
|
348
|
+
const storedAuthState = await readJsonFile(authStateFile);
|
|
349
|
+
if (storedAuthState === null) return null;
|
|
350
|
+
if (isAuthState(storedAuthState)) return storedAuthState;
|
|
351
|
+
await deleteFileIfExists(authStateFile);
|
|
352
|
+
return null;
|
|
373
353
|
};
|
|
374
354
|
const writeAuthState = async (authStateFile, authState) => {
|
|
375
|
-
|
|
355
|
+
await writeJsonFile(authStateFile, authState);
|
|
376
356
|
};
|
|
377
|
-
const toAuthState =
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
357
|
+
const toAuthState = session => ({
|
|
358
|
+
pluginId: PLUGIN_ID,
|
|
359
|
+
sessionToken: session.jwtToken,
|
|
360
|
+
expiresAt: session.expiresAt,
|
|
361
|
+
authenticatedAt: new Date().toISOString(),
|
|
362
|
+
userId: session.user.id,
|
|
363
|
+
email: session.user.email
|
|
384
364
|
});
|
|
385
365
|
const resolveStoredAuthState = async (worktree, config) => {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
return null;
|
|
390
|
-
}
|
|
391
|
-
if (Date.parse(authState.expiresAt) > Date.now()) {
|
|
392
|
-
return authState;
|
|
393
|
-
}
|
|
394
|
-
await deleteFileIfExists(authStateFile);
|
|
366
|
+
const authStateFile = path.resolve(worktree, config.authStatePath);
|
|
367
|
+
const authState = await readAuthState(authStateFile);
|
|
368
|
+
if (!authState) {
|
|
395
369
|
return null;
|
|
370
|
+
}
|
|
371
|
+
if (Date.parse(authState.expiresAt) > Date.now()) {
|
|
372
|
+
return authState;
|
|
373
|
+
}
|
|
374
|
+
await deleteFileIfExists(authStateFile);
|
|
375
|
+
return null;
|
|
396
376
|
};
|
|
397
|
-
export const buildSkillMarkdown =
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
};
|
|
412
|
-
const getSkillIdentifiers = (item) => {
|
|
413
|
-
const candidates = [item.skill.slug, item.publishedArtifact.frontmatterName, item.skill.name];
|
|
414
|
-
const seen = new Set();
|
|
415
|
-
return candidates.reduce((all, candidate) => {
|
|
416
|
-
const normalized = candidate.trim();
|
|
417
|
-
if (!normalized)
|
|
418
|
-
return all;
|
|
419
|
-
const cacheKey = normalized.toLowerCase();
|
|
420
|
-
if (seen.has(cacheKey))
|
|
421
|
-
return all;
|
|
422
|
-
seen.add(cacheKey);
|
|
423
|
-
all.push(normalized);
|
|
424
|
-
return all;
|
|
425
|
-
}, []);
|
|
377
|
+
export const buildSkillMarkdown = item => {
|
|
378
|
+
const artifactBody = item.publishedArtifact.markdownBody.trim();
|
|
379
|
+
const fallbackBody = item.publishedArtifact.renderedContent.trim();
|
|
380
|
+
const body = artifactBody || fallbackBody;
|
|
381
|
+
if (body.startsWith('---')) {
|
|
382
|
+
return body.endsWith('\n') ? body : `${body}\n`;
|
|
383
|
+
}
|
|
384
|
+
const name = formatSkillLabel(item);
|
|
385
|
+
const description = item.publishedArtifact.frontmatterDescription.trim() || item.skill.summary?.trim() || '';
|
|
386
|
+
const frontmatter = ['---', `name: ${toFrontmatterString(name)}`, `description: ${toFrontmatterString(description)}`, '---'].join('\n');
|
|
387
|
+
if (!body) {
|
|
388
|
+
return `${frontmatter}\n`;
|
|
389
|
+
}
|
|
390
|
+
return `${frontmatter}\n\n${body.endsWith('\n') ? body : `${body}\n`}`;
|
|
426
391
|
};
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
392
|
+
const getSkillIdentifiers = item => {
|
|
393
|
+
const candidates = [item.skill.slug, item.publishedArtifact.frontmatterName, item.skill.name];
|
|
394
|
+
const seen = new Set();
|
|
395
|
+
return candidates.reduce((all, candidate) => {
|
|
396
|
+
const normalized = candidate.trim();
|
|
397
|
+
if (!normalized) return all;
|
|
398
|
+
const cacheKey = normalized.toLowerCase();
|
|
399
|
+
if (seen.has(cacheKey)) return all;
|
|
400
|
+
seen.add(cacheKey);
|
|
401
|
+
all.push(normalized);
|
|
402
|
+
return all;
|
|
403
|
+
}, []);
|
|
404
|
+
};
|
|
405
|
+
const toPublishedSkillFacetSummary = facet => ({
|
|
406
|
+
slug: facet.slug,
|
|
407
|
+
label: facet.label,
|
|
408
|
+
description: facet.description ?? null
|
|
431
409
|
});
|
|
432
|
-
const toPublishedSkillTagSummary =
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
410
|
+
const toPublishedSkillTagSummary = tag => ({
|
|
411
|
+
slug: tag.slug,
|
|
412
|
+
label: tag.label,
|
|
413
|
+
description: tag.description ?? null,
|
|
414
|
+
facet: tag.facet ? toPublishedSkillFacetSummary(tag.facet) : null
|
|
437
415
|
});
|
|
438
|
-
const getPublishedSkillFacets =
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
continue;
|
|
446
|
-
facetsBySlug.set(tag.facet.slug, toPublishedSkillFacetSummary(tag.facet));
|
|
447
|
-
}
|
|
416
|
+
const getPublishedSkillFacets = items => {
|
|
417
|
+
const facetsBySlug = new Map();
|
|
418
|
+
for (const item of items) {
|
|
419
|
+
for (const tag of item.skill.tags) {
|
|
420
|
+
if (!tag.facet) continue;
|
|
421
|
+
if (facetsBySlug.has(tag.facet.slug)) continue;
|
|
422
|
+
facetsBySlug.set(tag.facet.slug, toPublishedSkillFacetSummary(tag.facet));
|
|
448
423
|
}
|
|
449
|
-
|
|
424
|
+
}
|
|
425
|
+
return [...facetsBySlug.values()].sort((left, right) => left.slug.localeCompare(right.slug));
|
|
450
426
|
};
|
|
451
|
-
const
|
|
427
|
+
const getPublishedSkillAssignmentCounts = items => items.reduce((counts, item) => {
|
|
428
|
+
if (item.assignmentSource === 'GLOBAL') {
|
|
429
|
+
return {
|
|
430
|
+
...counts,
|
|
431
|
+
global: counts.global + 1
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
if (item.assignmentSource === 'WORKSPACE') {
|
|
435
|
+
return {
|
|
436
|
+
...counts,
|
|
437
|
+
project: counts.project + 1
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
...counts,
|
|
442
|
+
other: counts.other + 1
|
|
443
|
+
};
|
|
444
|
+
}, {
|
|
445
|
+
global: 0,
|
|
446
|
+
project: 0,
|
|
447
|
+
other: 0
|
|
448
|
+
});
|
|
449
|
+
const getSkillContextKind = item => {
|
|
450
|
+
if (item.assignmentSource === 'GLOBAL') return 'global';
|
|
451
|
+
return 'project';
|
|
452
|
+
};
|
|
453
|
+
const getSkillPolicyLabel = (policy, contextKind) => {
|
|
454
|
+
if (policy === 'GLOBAL_CONTEXT') return 'GLOBAL_CONTEXT · active context only, not project-installable';
|
|
455
|
+
if (contextKind === 'global') return 'PROJECT_INSTALLABLE · active global assignment';
|
|
456
|
+
return 'PROJECT_INSTALLABLE · active project/workspace assignment';
|
|
457
|
+
};
|
|
458
|
+
const toPublishedSkillSummary = item => {
|
|
459
|
+
const contextKind = getSkillContextKind(item);
|
|
460
|
+
return {
|
|
452
461
|
skillSlug: item.skill.slug,
|
|
453
462
|
skillName: item.skill.name,
|
|
454
463
|
artifactName: item.publishedArtifact.frontmatterName,
|
|
455
464
|
artifactDescription: item.publishedArtifact.frontmatterDescription,
|
|
465
|
+
whenToUse: item.skill.whenToUse ?? null,
|
|
456
466
|
version: item.skillVersion.version,
|
|
457
467
|
assignmentSource: item.assignmentSource,
|
|
458
468
|
assignmentType: item.assignmentType,
|
|
@@ -462,1167 +472,1317 @@ const toPublishedSkillSummary = (item) => ({
|
|
|
462
472
|
publishedAt: item.publishedArtifact.publishedAt,
|
|
463
473
|
identifiers: getSkillIdentifiers(item),
|
|
464
474
|
tags: item.skill.tags.map(toPublishedSkillTagSummary),
|
|
475
|
+
contextKind,
|
|
476
|
+
installPolicy: item.skill.installPolicy,
|
|
477
|
+
policyLabel: getSkillPolicyLabel(item.skill.installPolicy, contextKind)
|
|
478
|
+
};
|
|
479
|
+
};
|
|
480
|
+
export const toPublishedSkillDetail = item => ({
|
|
481
|
+
...toPublishedSkillSummary(item),
|
|
482
|
+
skillId: item.skill.id,
|
|
483
|
+
skillVersionId: item.skillVersion.id,
|
|
484
|
+
artifactId: item.publishedArtifact.id,
|
|
485
|
+
markdownDocument: buildSkillMarkdown(item),
|
|
486
|
+
markdownBody: item.publishedArtifact.markdownBody,
|
|
487
|
+
renderedContent: item.publishedArtifact.renderedContent
|
|
465
488
|
});
|
|
466
|
-
export const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
workspace: payload.workspace,
|
|
479
|
-
directoryPath: payload.directoryPath,
|
|
480
|
-
rootSkillSeedPath: ROOT_SKILL_SEED_PATH,
|
|
481
|
-
availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS,
|
|
482
|
-
publishedSkillCount: payload.skills.length,
|
|
483
|
-
facets: getPublishedSkillFacets(payload.skills),
|
|
484
|
-
skills: payload.skills.map(toPublishedSkillSummary),
|
|
489
|
+
export const toPublishedSkillCatalog = payload => ({
|
|
490
|
+
pluginId: PLUGIN_ID,
|
|
491
|
+
runtimeMode: 'tool_fetch_only',
|
|
492
|
+
deliveryModel: 'backend_published_global_project_assignments',
|
|
493
|
+
workspace: payload.workspace,
|
|
494
|
+
directoryPath: payload.directoryPath,
|
|
495
|
+
rootSkillSeedPath: ROOT_SKILL_SEED_PATH,
|
|
496
|
+
availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS,
|
|
497
|
+
publishedSkillCount: payload.skills.length,
|
|
498
|
+
assignmentCounts: getPublishedSkillAssignmentCounts(payload.skills),
|
|
499
|
+
facets: getPublishedSkillFacets(payload.skills),
|
|
500
|
+
skills: payload.skills.map(toPublishedSkillSummary)
|
|
485
501
|
});
|
|
486
|
-
const normalizeSkillIdentifier =
|
|
487
|
-
const parseSkillIdentifiers =
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
if (seen.has(normalized))
|
|
496
|
-
return false;
|
|
497
|
-
seen.add(normalized);
|
|
498
|
-
return true;
|
|
499
|
-
});
|
|
502
|
+
const normalizeSkillIdentifier = value => value.trim().toLowerCase();
|
|
503
|
+
const parseSkillIdentifiers = value => {
|
|
504
|
+
const seen = new Set();
|
|
505
|
+
return value.split(/[\n,]/).map(item => item.trim()).filter(item => item.length > 0).filter(item => {
|
|
506
|
+
const normalized = normalizeSkillIdentifier(item);
|
|
507
|
+
if (seen.has(normalized)) return false;
|
|
508
|
+
seen.add(normalized);
|
|
509
|
+
return true;
|
|
510
|
+
});
|
|
500
511
|
};
|
|
501
|
-
const mergeSkillIdentifiers =
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
});
|
|
512
|
+
const mergeSkillIdentifiers = values => {
|
|
513
|
+
const seen = new Set();
|
|
514
|
+
return values.filter(value => {
|
|
515
|
+
const normalized = normalizeSkillIdentifier(value);
|
|
516
|
+
if (!normalized || seen.has(normalized)) return false;
|
|
517
|
+
seen.add(normalized);
|
|
518
|
+
return true;
|
|
519
|
+
});
|
|
510
520
|
};
|
|
511
|
-
const parseRequestedSkillArgs =
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
return mergeSkillIdentifiers(requestedSkills);
|
|
521
|
+
const parseRequestedSkillArgs = args => {
|
|
522
|
+
const requestedSkills = [];
|
|
523
|
+
if (typeof args.skill === 'string') {
|
|
524
|
+
const normalizedSkill = args.skill.trim();
|
|
525
|
+
if (normalizedSkill) requestedSkills.push(normalizedSkill);
|
|
526
|
+
}
|
|
527
|
+
if (typeof args.skills === 'string') {
|
|
528
|
+
requestedSkills.push(...parseSkillIdentifiers(args.skills));
|
|
529
|
+
}
|
|
530
|
+
return mergeSkillIdentifiers(requestedSkills);
|
|
522
531
|
};
|
|
523
532
|
export const selectPublishedSkills = (payload, identifiers) => {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
533
|
+
const itemsByIdentifier = new Map();
|
|
534
|
+
for (const item of payload.skills) {
|
|
535
|
+
for (const identifier of getSkillIdentifiers(item)) {
|
|
536
|
+
itemsByIdentifier.set(normalizeSkillIdentifier(identifier), item);
|
|
529
537
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
if (selectedKeys.has(matched.publishedArtifact.id)) {
|
|
540
|
-
continue;
|
|
541
|
-
}
|
|
542
|
-
selectedKeys.add(matched.publishedArtifact.id);
|
|
543
|
-
selectedItems.push(matched);
|
|
538
|
+
}
|
|
539
|
+
const selectedItems = [];
|
|
540
|
+
const selectedKeys = new Set();
|
|
541
|
+
const missingIdentifiers = [];
|
|
542
|
+
for (const identifier of identifiers) {
|
|
543
|
+
const matched = itemsByIdentifier.get(normalizeSkillIdentifier(identifier));
|
|
544
|
+
if (!matched) {
|
|
545
|
+
missingIdentifiers.push(identifier);
|
|
546
|
+
continue;
|
|
544
547
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
548
|
+
if (selectedKeys.has(matched.publishedArtifact.id)) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
selectedKeys.add(matched.publishedArtifact.id);
|
|
552
|
+
selectedItems.push(matched);
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
selectedItems,
|
|
556
|
+
missingIdentifiers
|
|
557
|
+
};
|
|
558
|
+
};
|
|
559
|
+
const truncateText = (value, maxLength) => {
|
|
560
|
+
const normalized = value.replace(/\s+/gu, ' ').trim();
|
|
561
|
+
if (normalized.length <= maxLength) return normalized;
|
|
562
|
+
return `${normalized.slice(0, Math.max(maxLength - 1, 0)).trimEnd()}…`;
|
|
563
|
+
};
|
|
564
|
+
const buildSkillCatalogLine = skill => {
|
|
565
|
+
const description = truncateText(skill.whenToUse || skill.artifactDescription || skill.skillName || skill.skillSlug, SYSTEM_NOTE_SKILL_DESCRIPTION_LIMIT);
|
|
566
|
+
return `- ${skill.artifactName || skill.skillName} (${skill.skillSlug}, ${skill.contextKind.toUpperCase()}): ${description}`;
|
|
549
567
|
};
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
};
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
568
|
+
const buildSkillDetailSnippetLine = detail => {
|
|
569
|
+
const body = detail.markdownBody || detail.renderedContent || detail.markdownDocument;
|
|
570
|
+
return `- ${detail.artifactName || detail.skillName}: ${truncateText(body, 700)}`;
|
|
571
|
+
};
|
|
572
|
+
const buildSystemNote = (result, config, details) => {
|
|
573
|
+
if (!result.fetchResult.ok) return null;
|
|
574
|
+
const catalog = toPublishedSkillCatalog(result.fetchResult.payload);
|
|
575
|
+
const skillNames = catalog.skills.map(skill => skill.artifactName || skill.skillName || skill.skillSlug);
|
|
576
|
+
const renderedSkillNames = skillNames.length > 0 ? skillNames.slice(0, SYSTEM_NOTE_SKILL_NAME_LIMIT).join(', ') : 'none';
|
|
577
|
+
const remainingCount = Math.max(skillNames.length - SYSTEM_NOTE_SKILL_NAME_LIMIT, 0);
|
|
578
|
+
const renderedCountSuffix = remainingCount > 0 ? ` (+${remainingCount} more)` : '';
|
|
579
|
+
const globalSkills = catalog.skills.filter(skill => skill.contextKind === 'global').slice(0, 8).map(buildSkillCatalogLine);
|
|
580
|
+
const projectSkills = catalog.skills.filter(skill => skill.contextKind === 'project').slice(0, 5).map(buildSkillCatalogLine);
|
|
581
|
+
const detailLines = details.slice(0, SYSTEM_NOTE_DETAIL_LIMIT).map(buildSkillDetailSnippetLine);
|
|
582
|
+
const detailBlock = detailLines.length > 0 ? ` Loaded body snippets (capped):\n${truncateText(detailLines.join('\n'), SYSTEM_NOTE_DETAIL_CHAR_LIMIT)}` : '';
|
|
583
|
+
return [`opencode-wizard published skills are available from backend runtime delivery for workspace ${result.fetchResult.payload.workspace.slug}.`, `Current directory: ${result.directoryPath}.`, `Published skills for this scope: ${renderedSkillNames}${renderedCountSuffix}; counts: ${catalog.assignmentCounts.global} global, ${catalog.assignmentCounts.project} project, ${catalog.assignmentCounts.other} other.`, 'Catalog lines use short whenToUse guidance when available; fetch the full skill only when that guidance matches the task.', 'GLOBAL_CONTEXT skills are active context skills and are not project-installable; PROJECT_INSTALLABLE skills can be assigned globally or to project/workspace scopes; assignment rows decide which skills are active here.', globalSkills.length > 0 ? `Global context skills:\n${globalSkills.join('\n')}` : 'Global context skills: none.', projectSkills.length > 0 ? `Project-scoped active skills:\n${projectSkills.join('\n')}` : 'Project-scoped active skills: none.', detailBlock, 'Use opencode_wizard_published_skills_fetch for one or multiple skills.', `Root source seed path remains non-runtime input only: ${config.rootSkillSeedPath}/**.`].filter(line => line.length > 0).join(' ');
|
|
584
|
+
};
|
|
585
|
+
const toWorkspaceResolutionOutput = resolution => ({
|
|
586
|
+
requestedDirectory: resolution.requestedDirectory,
|
|
587
|
+
repositoryRoot: resolution.repositoryRoot,
|
|
588
|
+
repositoryUrl: resolution.repositoryUrl,
|
|
589
|
+
fallbackWorkspaceSlug: resolution.fallbackWorkspaceSlug,
|
|
590
|
+
directoryPath: resolution.directoryPath
|
|
571
591
|
});
|
|
572
|
-
const toWorkspaceResolutionMetadata =
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
592
|
+
const toWorkspaceResolutionMetadata = resolution => ({
|
|
593
|
+
directoryPath: resolution.directoryPath,
|
|
594
|
+
repositoryRoot: resolution.repositoryRoot,
|
|
595
|
+
repositoryUrl: resolution.repositoryUrl ?? '',
|
|
596
|
+
fallbackWorkspaceSlug: resolution.fallbackWorkspaceSlug ?? ''
|
|
577
597
|
});
|
|
578
598
|
const formatStatusOutput = async (worktree, config, publishedSkillsResult, loginBootstrapSnapshot) => {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS,
|
|
615
|
-
};
|
|
616
|
-
if (!publishedSkillsResult.fetchResult.ok) {
|
|
617
|
-
return JSON.stringify({
|
|
618
|
-
...base,
|
|
619
|
-
message: publishedSkillsResult.fetchResult.message,
|
|
620
|
-
}, null, 2);
|
|
621
|
-
}
|
|
599
|
+
const authState = await resolveStoredAuthState(worktree, config);
|
|
600
|
+
const base = {
|
|
601
|
+
pluginId: PLUGIN_ID,
|
|
602
|
+
runtimeMode: 'tool_fetch_only',
|
|
603
|
+
nativeSkillsUrlCompatibility: NATIVE_SKILLS_URL_COMPATIBILITY,
|
|
604
|
+
backendOrigin: config.backendOrigin,
|
|
605
|
+
graphqlUrl: config.graphqlUrl,
|
|
606
|
+
fallbackWorkspaceSlug: config.fallbackWorkspaceSlug,
|
|
607
|
+
workspaceResolution: toWorkspaceResolutionOutput(publishedSkillsResult.workspaceResolution),
|
|
608
|
+
rootSkillSeedPath: config.rootSkillSeedPath,
|
|
609
|
+
authStatePath: config.authStatePath,
|
|
610
|
+
requestedDirectoryPath: publishedSkillsResult.directoryPath,
|
|
611
|
+
authMode: publishedSkillsResult.fetchResult.authMode,
|
|
612
|
+
authState: authState === null ? null : {
|
|
613
|
+
email: authState.email,
|
|
614
|
+
userId: authState.userId,
|
|
615
|
+
authenticatedAt: authState.authenticatedAt,
|
|
616
|
+
expiresAt: authState.expiresAt
|
|
617
|
+
},
|
|
618
|
+
loginBootstrap: loginBootstrapSnapshot.status === 'idle' ? null : {
|
|
619
|
+
status: loginBootstrapSnapshot.status,
|
|
620
|
+
trigger: loginBootstrapSnapshot.trigger,
|
|
621
|
+
startedAt: loginBootstrapSnapshot.startedAt,
|
|
622
|
+
expiresAt: loginBootstrapSnapshot.expiresAt,
|
|
623
|
+
browserUrl: loginBootstrapSnapshot.browserUrl,
|
|
624
|
+
browserOpenError: loginBootstrapSnapshot.browserOpenError,
|
|
625
|
+
email: loginBootstrapSnapshot.email,
|
|
626
|
+
message: loginBootstrapSnapshot.message
|
|
627
|
+
},
|
|
628
|
+
status: publishedSkillsResult.fetchResult.status,
|
|
629
|
+
fetchedAt: publishedSkillsResult.fetchResult.fetchedAt,
|
|
630
|
+
source: publishedSkillsResult.fetchResult.source,
|
|
631
|
+
availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS
|
|
632
|
+
};
|
|
633
|
+
if (!publishedSkillsResult.fetchResult.ok) {
|
|
622
634
|
return JSON.stringify({
|
|
623
|
-
|
|
624
|
-
|
|
635
|
+
...base,
|
|
636
|
+
message: publishedSkillsResult.fetchResult.message
|
|
625
637
|
}, null, 2);
|
|
638
|
+
}
|
|
639
|
+
return JSON.stringify({
|
|
640
|
+
...base,
|
|
641
|
+
...toPublishedSkillCatalog(publishedSkillsResult.fetchResult.payload)
|
|
642
|
+
}, null, 2);
|
|
626
643
|
};
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
return
|
|
644
|
+
export const toPluginAuthStateSummary = authState => {
|
|
645
|
+
if (!authState) {
|
|
646
|
+
return {
|
|
647
|
+
status: 'missing',
|
|
648
|
+
email: null,
|
|
649
|
+
userId: null,
|
|
650
|
+
authenticatedAt: null,
|
|
651
|
+
expiresAt: null
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
status: 'authenticated',
|
|
656
|
+
email: authState.email,
|
|
657
|
+
userId: authState.userId,
|
|
658
|
+
authenticatedAt: authState.authenticatedAt,
|
|
659
|
+
expiresAt: authState.expiresAt
|
|
660
|
+
};
|
|
630
661
|
};
|
|
631
|
-
const
|
|
632
|
-
|
|
662
|
+
export const resolvePluginStatusSnapshot = async ({
|
|
663
|
+
worktree,
|
|
664
|
+
directory,
|
|
665
|
+
signal
|
|
666
|
+
}) => {
|
|
667
|
+
const config = await resolveConfig(worktree);
|
|
668
|
+
const workspaceResolution = await resolveWorkspace({
|
|
669
|
+
config,
|
|
670
|
+
directory
|
|
671
|
+
});
|
|
672
|
+
const [authState, fetchResult] = await Promise.all([resolveStoredAuthState(worktree, config), fetchPublishedSkillsCatalog(worktree, config, workspaceResolution, signal)]);
|
|
673
|
+
return {
|
|
674
|
+
pluginId: PLUGIN_ID,
|
|
675
|
+
runtimeMode: 'tool_fetch_only',
|
|
676
|
+
nativeSkillsUrlCompatibility: NATIVE_SKILLS_URL_COMPATIBILITY,
|
|
677
|
+
backendOrigin: config.backendOrigin,
|
|
678
|
+
graphqlUrl: config.graphqlUrl,
|
|
679
|
+
fallbackWorkspaceSlug: config.fallbackWorkspaceSlug,
|
|
680
|
+
workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
|
|
681
|
+
rootSkillSeedPath: config.rootSkillSeedPath,
|
|
682
|
+
authStatePath: config.authStatePath,
|
|
683
|
+
authState: toPluginAuthStateSummary(authState),
|
|
684
|
+
status: fetchResult.status,
|
|
685
|
+
authMode: fetchResult.authMode,
|
|
686
|
+
fetchedAt: fetchResult.fetchedAt,
|
|
687
|
+
source: fetchResult.source,
|
|
688
|
+
availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS,
|
|
689
|
+
message: fetchResult.ok ? null : fetchResult.message,
|
|
690
|
+
catalog: fetchResult.ok ? toPublishedSkillCatalog(fetchResult.payload) : null
|
|
691
|
+
};
|
|
633
692
|
};
|
|
634
|
-
const
|
|
635
|
-
|
|
693
|
+
const isUnauthorizedGraphQlMessage = message => {
|
|
694
|
+
const normalizedMessage = message.toLowerCase();
|
|
695
|
+
return normalizedMessage.includes('not authorized') || normalizedMessage.includes('unauthorized');
|
|
636
696
|
};
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
return null;
|
|
640
|
-
const candidate = 'message' in value ? value.message : null;
|
|
641
|
-
return typeof candidate === 'string' ? candidate : null;
|
|
697
|
+
const createRandomBase64Url = bytes => {
|
|
698
|
+
return crypto.randomBytes(bytes).toString('base64url');
|
|
642
699
|
};
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
setTimeout(resolve, milliseconds);
|
|
646
|
-
});
|
|
700
|
+
const createCodeChallenge = codeVerifier => {
|
|
701
|
+
return crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
|
647
702
|
};
|
|
648
|
-
const
|
|
649
|
-
|
|
703
|
+
const getMessageFromUnknownPayload = value => {
|
|
704
|
+
if (!value || typeof value !== 'object') return null;
|
|
705
|
+
const candidate = 'message' in value ? value.message : null;
|
|
706
|
+
return typeof candidate === 'string' ? candidate : null;
|
|
650
707
|
};
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
708
|
+
const wait = async milliseconds => {
|
|
709
|
+
await new Promise(resolve => {
|
|
710
|
+
setTimeout(resolve, milliseconds);
|
|
711
|
+
});
|
|
712
|
+
};
|
|
713
|
+
const shouldRetryPresenceEvent = status => {
|
|
714
|
+
return status === 408 || status === 429 || status >= 500;
|
|
715
|
+
};
|
|
716
|
+
const fetchOidcDiscoveryDocument = async signal => {
|
|
717
|
+
const discoveryUrl = `${OIDC_ISSUER.replace(/\/+$/, '')}/.well-known/openid-configuration`;
|
|
718
|
+
const response = await fetch(discoveryUrl, {
|
|
719
|
+
method: 'GET',
|
|
720
|
+
signal
|
|
721
|
+
});
|
|
722
|
+
if (!response.ok) {
|
|
723
|
+
throw new Error(`OIDC discovery failed with HTTP ${response.status}.`);
|
|
724
|
+
}
|
|
725
|
+
return await response.json();
|
|
661
726
|
};
|
|
662
727
|
const sendHtmlResponse = (response, statusCode, title, message) => {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
};
|
|
669
|
-
const startLocalCallbackServer = async ({
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
728
|
+
response.writeHead(statusCode, {
|
|
729
|
+
'content-type': 'text/html; charset=utf-8',
|
|
730
|
+
'cache-control': 'no-store'
|
|
731
|
+
});
|
|
732
|
+
response.end(`<!doctype html><html><head><meta charset="utf-8"><title>${title}</title></head><body><h1>${title}</h1><p>${message}</p><p>You can close this window and return to OpenCode.</p></body></html>`);
|
|
733
|
+
};
|
|
734
|
+
const startLocalCallbackServer = async ({
|
|
735
|
+
expectedState,
|
|
736
|
+
signal
|
|
737
|
+
}) => {
|
|
738
|
+
let settled = false;
|
|
739
|
+
let resolvePayload = () => undefined;
|
|
740
|
+
let rejectPayload = () => undefined;
|
|
741
|
+
const callbackPromise = new Promise((resolve, reject) => {
|
|
742
|
+
resolvePayload = resolve;
|
|
743
|
+
rejectPayload = reject;
|
|
744
|
+
});
|
|
745
|
+
const finalize = payload => {
|
|
746
|
+
if (settled) return;
|
|
747
|
+
settled = true;
|
|
748
|
+
resolvePayload(payload);
|
|
749
|
+
};
|
|
750
|
+
const fail = reason => {
|
|
751
|
+
if (settled) return;
|
|
752
|
+
settled = true;
|
|
753
|
+
rejectPayload(reason);
|
|
754
|
+
};
|
|
755
|
+
const server = http.createServer((request, response) => {
|
|
756
|
+
const requestUrl = new URL(request.url ?? '/', OIDC_CALLBACK_ORIGIN);
|
|
757
|
+
if (requestUrl.pathname !== OIDC_CALLBACK_PATH) {
|
|
758
|
+
sendHtmlResponse(response, 404, 'opencode-wizard plugin login', 'Unknown callback path.');
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const error = requestUrl.searchParams.get('error');
|
|
762
|
+
const errorDescription = requestUrl.searchParams.get('error_description');
|
|
763
|
+
if (error) {
|
|
764
|
+
const message = errorDescription ?? error;
|
|
765
|
+
sendHtmlResponse(response, 400, 'opencode-wizard plugin login failed', message);
|
|
766
|
+
finalize({
|
|
767
|
+
status: 'error',
|
|
768
|
+
message
|
|
769
|
+
});
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
const state = requestUrl.searchParams.get('state');
|
|
773
|
+
const code = requestUrl.searchParams.get('code');
|
|
774
|
+
if (!state || state !== expectedState) {
|
|
775
|
+
sendHtmlResponse(response, 400, 'opencode-wizard plugin login failed', 'OAuth state did not match the login request.');
|
|
776
|
+
finalize({
|
|
777
|
+
status: 'error',
|
|
778
|
+
message: 'OAuth state did not match the login request.'
|
|
779
|
+
});
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
if (!code) {
|
|
783
|
+
sendHtmlResponse(response, 400, 'opencode-wizard plugin login failed', 'OAuth callback did not include an authorization code.');
|
|
784
|
+
finalize({
|
|
785
|
+
status: 'error',
|
|
786
|
+
message: 'OAuth callback did not include an authorization code.'
|
|
787
|
+
});
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
sendHtmlResponse(response, 200, 'opencode-wizard plugin callback received', 'Callback received. OpenCode is finalizing the backend session now.');
|
|
791
|
+
finalize({
|
|
792
|
+
status: 'success',
|
|
793
|
+
code,
|
|
794
|
+
state
|
|
676
795
|
});
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
if (settled)
|
|
685
|
-
return;
|
|
686
|
-
settled = true;
|
|
687
|
-
rejectPayload(reason);
|
|
688
|
-
};
|
|
689
|
-
const server = http.createServer((request, response) => {
|
|
690
|
-
const requestUrl = new URL(request.url ?? '/', OIDC_CALLBACK_ORIGIN);
|
|
691
|
-
if (requestUrl.pathname !== OIDC_CALLBACK_PATH) {
|
|
692
|
-
sendHtmlResponse(response, 404, 'opencode-wizard plugin login', 'Unknown callback path.');
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
const error = requestUrl.searchParams.get('error');
|
|
696
|
-
const errorDescription = requestUrl.searchParams.get('error_description');
|
|
796
|
+
});
|
|
797
|
+
server.on('error', error => {
|
|
798
|
+
fail(error instanceof Error ? error : new Error('Failed to start local OAuth callback server.'));
|
|
799
|
+
});
|
|
800
|
+
const close = async () => {
|
|
801
|
+
await new Promise((resolve, reject) => {
|
|
802
|
+
server.close(error => {
|
|
697
803
|
if (error) {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
finalize({
|
|
701
|
-
status: 'error',
|
|
702
|
-
message,
|
|
703
|
-
});
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
706
|
-
const state = requestUrl.searchParams.get('state');
|
|
707
|
-
const code = requestUrl.searchParams.get('code');
|
|
708
|
-
if (!state || state !== expectedState) {
|
|
709
|
-
sendHtmlResponse(response, 400, 'opencode-wizard plugin login failed', 'OAuth state did not match the login request.');
|
|
710
|
-
finalize({
|
|
711
|
-
status: 'error',
|
|
712
|
-
message: 'OAuth state did not match the login request.',
|
|
713
|
-
});
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
if (!code) {
|
|
717
|
-
sendHtmlResponse(response, 400, 'opencode-wizard plugin login failed', 'OAuth callback did not include an authorization code.');
|
|
718
|
-
finalize({
|
|
719
|
-
status: 'error',
|
|
720
|
-
message: 'OAuth callback did not include an authorization code.',
|
|
721
|
-
});
|
|
722
|
-
return;
|
|
804
|
+
reject(error);
|
|
805
|
+
return;
|
|
723
806
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
status: 'success',
|
|
727
|
-
code,
|
|
728
|
-
state,
|
|
729
|
-
});
|
|
730
|
-
});
|
|
731
|
-
server.on('error', (error) => {
|
|
732
|
-
fail(error instanceof Error ? error : new Error('Failed to start local OAuth callback server.'));
|
|
807
|
+
resolve();
|
|
808
|
+
});
|
|
733
809
|
});
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
810
|
+
};
|
|
811
|
+
await new Promise((resolve, reject) => {
|
|
812
|
+
server.listen(24953, 'localhost', () => resolve());
|
|
813
|
+
server.once('error', reject);
|
|
814
|
+
});
|
|
815
|
+
signal.addEventListener('abort', () => {
|
|
816
|
+
fail(signal.reason instanceof Error ? signal.reason : new Error('OAuth login aborted.'));
|
|
817
|
+
void close().catch(() => undefined);
|
|
818
|
+
}, {
|
|
819
|
+
once: true
|
|
820
|
+
});
|
|
821
|
+
return {
|
|
822
|
+
callbackPromise,
|
|
823
|
+
close
|
|
824
|
+
};
|
|
825
|
+
};
|
|
826
|
+
const fetchPublishedSkillsGraphQl = async ({
|
|
827
|
+
worktree,
|
|
828
|
+
config,
|
|
829
|
+
query,
|
|
830
|
+
variables,
|
|
831
|
+
signal,
|
|
832
|
+
onAuthStateChanged
|
|
833
|
+
}) => {
|
|
834
|
+
const authState = await resolveStoredAuthState(worktree, config);
|
|
835
|
+
const fetchedAt = new Date().toISOString();
|
|
836
|
+
if (!authState) {
|
|
837
|
+
return {
|
|
838
|
+
ok: false,
|
|
839
|
+
result: {
|
|
840
|
+
ok: false,
|
|
841
|
+
status: 'missing_auth',
|
|
842
|
+
authMode: 'missing',
|
|
843
|
+
message: 'No plugin session is stored. Interactive opencode_wizard_published_skills_fetch can bootstrap browser login automatically when needed.',
|
|
844
|
+
fetchedAt,
|
|
845
|
+
source: 'network'
|
|
846
|
+
}
|
|
744
847
|
};
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
848
|
+
}
|
|
849
|
+
let response;
|
|
850
|
+
try {
|
|
851
|
+
response = await fetch(config.graphqlUrl, {
|
|
852
|
+
method: 'POST',
|
|
853
|
+
headers: {
|
|
854
|
+
'content-type': 'application/json',
|
|
855
|
+
authorization: `Bearer ${authState.sessionToken}`
|
|
856
|
+
},
|
|
857
|
+
body: JSON.stringify({
|
|
858
|
+
query,
|
|
859
|
+
variables
|
|
860
|
+
}),
|
|
861
|
+
signal
|
|
748
862
|
});
|
|
749
|
-
|
|
750
|
-
fail(signal.reason instanceof Error ? signal.reason : new Error('OAuth login aborted.'));
|
|
751
|
-
void close().catch(() => undefined);
|
|
752
|
-
}, { once: true });
|
|
863
|
+
} catch (error) {
|
|
753
864
|
return {
|
|
754
|
-
|
|
755
|
-
|
|
865
|
+
ok: false,
|
|
866
|
+
result: {
|
|
867
|
+
ok: false,
|
|
868
|
+
status: 'request_failed',
|
|
869
|
+
authMode: 'session',
|
|
870
|
+
message: error instanceof Error ? error.message : 'Unknown fetch error',
|
|
871
|
+
fetchedAt,
|
|
872
|
+
source: 'network'
|
|
873
|
+
}
|
|
756
874
|
};
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
}
|
|
817
|
-
if (!response.ok) {
|
|
818
|
-
return {
|
|
819
|
-
ok: false,
|
|
820
|
-
result: {
|
|
821
|
-
ok: false,
|
|
822
|
-
status: 'request_failed',
|
|
823
|
-
authMode: 'session',
|
|
824
|
-
message: `GraphQL request failed with HTTP ${response.status}.`,
|
|
825
|
-
fetchedAt,
|
|
826
|
-
source: 'network',
|
|
827
|
-
},
|
|
828
|
-
};
|
|
829
|
-
}
|
|
830
|
-
let body;
|
|
831
|
-
try {
|
|
832
|
-
body = (await response.json());
|
|
833
|
-
}
|
|
834
|
-
catch (error) {
|
|
835
|
-
return {
|
|
836
|
-
ok: false,
|
|
837
|
-
result: {
|
|
838
|
-
ok: false,
|
|
839
|
-
status: 'request_failed',
|
|
840
|
-
authMode: 'session',
|
|
841
|
-
message: `GraphQL response was not valid JSON: ${error instanceof Error ? error.message : 'Unknown parse error'}`,
|
|
842
|
-
fetchedAt,
|
|
843
|
-
source: 'network',
|
|
844
|
-
},
|
|
845
|
-
};
|
|
846
|
-
}
|
|
847
|
-
if (body.errors?.length) {
|
|
848
|
-
const message = body.errors.map((error) => error.message).join('; ');
|
|
849
|
-
if (body.errors.some((error) => isUnauthorizedGraphQlMessage(error.message))) {
|
|
850
|
-
await deleteFileIfExists(path.resolve(worktree, config.authStatePath));
|
|
851
|
-
onAuthStateChanged?.();
|
|
852
|
-
return {
|
|
853
|
-
ok: false,
|
|
854
|
-
result: {
|
|
855
|
-
ok: false,
|
|
856
|
-
status: 'missing_auth',
|
|
857
|
-
authMode: 'session',
|
|
858
|
-
message: 'Stored plugin session is no longer valid. Retry opencode_wizard_published_skills_fetch to bootstrap a fresh browser login automatically.',
|
|
859
|
-
fetchedAt,
|
|
860
|
-
source: 'network',
|
|
861
|
-
},
|
|
862
|
-
};
|
|
875
|
+
}
|
|
876
|
+
if (response.status === 401 || response.status === 403) {
|
|
877
|
+
await deleteFileIfExists(path.resolve(worktree, config.authStatePath));
|
|
878
|
+
onAuthStateChanged?.();
|
|
879
|
+
return {
|
|
880
|
+
ok: false,
|
|
881
|
+
result: {
|
|
882
|
+
ok: false,
|
|
883
|
+
status: 'missing_auth',
|
|
884
|
+
authMode: 'session',
|
|
885
|
+
message: 'Stored plugin session was rejected by the backend. Retry opencode_wizard_published_skills_fetch to bootstrap a fresh browser login automatically.',
|
|
886
|
+
fetchedAt,
|
|
887
|
+
source: 'network'
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
if (!response.ok) {
|
|
892
|
+
return {
|
|
893
|
+
ok: false,
|
|
894
|
+
result: {
|
|
895
|
+
ok: false,
|
|
896
|
+
status: 'request_failed',
|
|
897
|
+
authMode: 'session',
|
|
898
|
+
message: `GraphQL request failed with HTTP ${response.status}.`,
|
|
899
|
+
fetchedAt,
|
|
900
|
+
source: 'network'
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
let body;
|
|
905
|
+
try {
|
|
906
|
+
body = await response.json();
|
|
907
|
+
} catch (error) {
|
|
908
|
+
return {
|
|
909
|
+
ok: false,
|
|
910
|
+
result: {
|
|
911
|
+
ok: false,
|
|
912
|
+
status: 'request_failed',
|
|
913
|
+
authMode: 'session',
|
|
914
|
+
message: `GraphQL response was not valid JSON: ${error instanceof Error ? error.message : 'Unknown parse error'}`,
|
|
915
|
+
fetchedAt,
|
|
916
|
+
source: 'network'
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
if (body.errors?.length) {
|
|
921
|
+
const message = body.errors.map(error => error.message).join('; ');
|
|
922
|
+
if (body.errors.some(error => isUnauthorizedGraphQlMessage(error.message))) {
|
|
923
|
+
await deleteFileIfExists(path.resolve(worktree, config.authStatePath));
|
|
924
|
+
onAuthStateChanged?.();
|
|
925
|
+
return {
|
|
926
|
+
ok: false,
|
|
927
|
+
result: {
|
|
928
|
+
ok: false,
|
|
929
|
+
status: 'missing_auth',
|
|
930
|
+
authMode: 'session',
|
|
931
|
+
message: 'Stored plugin session is no longer valid. Retry opencode_wizard_published_skills_fetch to bootstrap a fresh browser login automatically.',
|
|
932
|
+
fetchedAt,
|
|
933
|
+
source: 'network'
|
|
863
934
|
}
|
|
864
|
-
|
|
865
|
-
ok: false,
|
|
866
|
-
result: {
|
|
867
|
-
ok: false,
|
|
868
|
-
status: 'request_failed',
|
|
869
|
-
authMode: 'session',
|
|
870
|
-
message,
|
|
871
|
-
fetchedAt,
|
|
872
|
-
source: 'network',
|
|
873
|
-
},
|
|
874
|
-
};
|
|
875
|
-
}
|
|
876
|
-
if (!body.data) {
|
|
877
|
-
return {
|
|
878
|
-
ok: false,
|
|
879
|
-
result: {
|
|
880
|
-
ok: false,
|
|
881
|
-
status: 'request_failed',
|
|
882
|
-
authMode: 'session',
|
|
883
|
-
message: 'GraphQL response did not include data.',
|
|
884
|
-
fetchedAt,
|
|
885
|
-
source: 'network',
|
|
886
|
-
},
|
|
887
|
-
};
|
|
935
|
+
};
|
|
888
936
|
}
|
|
889
937
|
return {
|
|
890
|
-
|
|
891
|
-
|
|
938
|
+
ok: false,
|
|
939
|
+
result: {
|
|
940
|
+
ok: false,
|
|
941
|
+
status: 'request_failed',
|
|
942
|
+
authMode: 'session',
|
|
943
|
+
message,
|
|
892
944
|
fetchedAt,
|
|
945
|
+
source: 'network'
|
|
946
|
+
}
|
|
893
947
|
};
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
const response = await fetchPublishedSkillsGraphQl({
|
|
897
|
-
worktree,
|
|
898
|
-
config,
|
|
899
|
-
query: PUBLISHED_SKILLS_CATALOG_QUERY,
|
|
900
|
-
variables: {
|
|
901
|
-
input: toDeliveryInput(resolution),
|
|
902
|
-
},
|
|
903
|
-
signal,
|
|
904
|
-
onAuthStateChanged,
|
|
905
|
-
});
|
|
906
|
-
if (!response.ok) {
|
|
907
|
-
return response.result;
|
|
908
|
-
}
|
|
909
|
-
const payload = response.data.pluginPublishedSkills;
|
|
910
|
-
if (!payload) {
|
|
911
|
-
return {
|
|
912
|
-
ok: false,
|
|
913
|
-
status: 'request_failed',
|
|
914
|
-
authMode: 'session',
|
|
915
|
-
message: 'GraphQL response did not include pluginPublishedSkills.',
|
|
916
|
-
fetchedAt: response.fetchedAt,
|
|
917
|
-
source: 'network',
|
|
918
|
-
};
|
|
919
|
-
}
|
|
948
|
+
}
|
|
949
|
+
if (!body.data) {
|
|
920
950
|
return {
|
|
921
|
-
|
|
922
|
-
|
|
951
|
+
ok: false,
|
|
952
|
+
result: {
|
|
953
|
+
ok: false,
|
|
954
|
+
status: 'request_failed',
|
|
923
955
|
authMode: 'session',
|
|
924
|
-
|
|
925
|
-
fetchedAt
|
|
926
|
-
source: 'network'
|
|
956
|
+
message: 'GraphQL response did not include data.',
|
|
957
|
+
fetchedAt,
|
|
958
|
+
source: 'network'
|
|
959
|
+
}
|
|
927
960
|
};
|
|
961
|
+
}
|
|
962
|
+
return {
|
|
963
|
+
ok: true,
|
|
964
|
+
data: body.data,
|
|
965
|
+
fetchedAt
|
|
966
|
+
};
|
|
928
967
|
};
|
|
929
|
-
const
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
}
|
|
946
|
-
const artifact = response.data.pluginPublishedSkillVersionArtifact;
|
|
947
|
-
if (!artifact) {
|
|
948
|
-
return {
|
|
949
|
-
ok: false,
|
|
950
|
-
result: {
|
|
951
|
-
ok: false,
|
|
952
|
-
status: 'not_found',
|
|
953
|
-
authMode: 'session',
|
|
954
|
-
message: 'Published skill detail is not effective for the current scope.',
|
|
955
|
-
fetchedAt: response.fetchedAt,
|
|
956
|
-
source: 'network',
|
|
957
|
-
},
|
|
958
|
-
};
|
|
959
|
-
}
|
|
968
|
+
const fetchPublishedSkillsCatalog = async (worktree, config, resolution, signal, onAuthStateChanged) => {
|
|
969
|
+
const response = await fetchPublishedSkillsGraphQl({
|
|
970
|
+
worktree,
|
|
971
|
+
config,
|
|
972
|
+
query: PUBLISHED_SKILLS_CATALOG_QUERY,
|
|
973
|
+
variables: {
|
|
974
|
+
input: toDeliveryInput(resolution)
|
|
975
|
+
},
|
|
976
|
+
signal,
|
|
977
|
+
onAuthStateChanged
|
|
978
|
+
});
|
|
979
|
+
if (!response.ok) {
|
|
980
|
+
return response.result;
|
|
981
|
+
}
|
|
982
|
+
const payload = response.data.pluginPublishedSkills;
|
|
983
|
+
if (!payload) {
|
|
960
984
|
return {
|
|
961
|
-
|
|
962
|
-
|
|
985
|
+
ok: false,
|
|
986
|
+
status: 'request_failed',
|
|
987
|
+
authMode: 'session',
|
|
988
|
+
message: 'GraphQL response did not include pluginPublishedSkills.',
|
|
989
|
+
fetchedAt: response.fetchedAt,
|
|
990
|
+
source: 'network'
|
|
963
991
|
};
|
|
992
|
+
}
|
|
993
|
+
return {
|
|
994
|
+
ok: true,
|
|
995
|
+
status: 'ready',
|
|
996
|
+
authMode: 'session',
|
|
997
|
+
payload,
|
|
998
|
+
fetchedAt: response.fetchedAt,
|
|
999
|
+
source: 'network'
|
|
1000
|
+
};
|
|
964
1001
|
};
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1002
|
+
const fetchPublishedSkillDetail = async ({
|
|
1003
|
+
worktree,
|
|
1004
|
+
config,
|
|
1005
|
+
resolution,
|
|
1006
|
+
skillVersionId,
|
|
1007
|
+
signal,
|
|
1008
|
+
onAuthStateChanged,
|
|
1009
|
+
purpose
|
|
1010
|
+
}) => {
|
|
1011
|
+
const response = await fetchPublishedSkillsGraphQl({
|
|
1012
|
+
worktree,
|
|
1013
|
+
config,
|
|
1014
|
+
query: PUBLISHED_SKILL_DETAIL_QUERY,
|
|
1015
|
+
variables: {
|
|
1016
|
+
input: {
|
|
1017
|
+
...toDeliveryInput(resolution),
|
|
1018
|
+
skillVersionId,
|
|
1019
|
+
purpose
|
|
1020
|
+
}
|
|
971
1021
|
},
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
expectedState,
|
|
981
|
-
signal,
|
|
982
|
-
});
|
|
983
|
-
const browserUrl = new URL(discovery.authorization_endpoint);
|
|
984
|
-
browserUrl.searchParams.set('client_id', OIDC_CLIENT_ID);
|
|
985
|
-
browserUrl.searchParams.set('response_type', 'code');
|
|
986
|
-
browserUrl.searchParams.set('redirect_uri', OIDC_CALLBACK_URL);
|
|
987
|
-
browserUrl.searchParams.set('response_mode', 'query');
|
|
988
|
-
browserUrl.searchParams.set('scope', OIDC_SCOPES.join(' '));
|
|
989
|
-
browserUrl.searchParams.set('code_challenge', codeChallenge);
|
|
990
|
-
browserUrl.searchParams.set('code_challenge_method', 'S256');
|
|
991
|
-
browserUrl.searchParams.set('state', expectedState);
|
|
1022
|
+
signal,
|
|
1023
|
+
onAuthStateChanged
|
|
1024
|
+
});
|
|
1025
|
+
if (!response.ok) {
|
|
1026
|
+
return response;
|
|
1027
|
+
}
|
|
1028
|
+
const artifact = response.data.pluginPublishedSkillVersionArtifact;
|
|
1029
|
+
if (!artifact) {
|
|
992
1030
|
return {
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1031
|
+
ok: false,
|
|
1032
|
+
result: {
|
|
1033
|
+
ok: false,
|
|
1034
|
+
status: 'not_found',
|
|
1035
|
+
authMode: 'session',
|
|
1036
|
+
message: 'Published skill detail is not effective for the current scope.',
|
|
1037
|
+
fetchedAt: response.fetchedAt,
|
|
1038
|
+
source: 'network'
|
|
1039
|
+
}
|
|
999
1040
|
};
|
|
1041
|
+
}
|
|
1042
|
+
return {
|
|
1043
|
+
ok: true,
|
|
1044
|
+
artifact
|
|
1045
|
+
};
|
|
1000
1046
|
};
|
|
1001
|
-
const
|
|
1002
|
-
|
|
1047
|
+
const toFetchFailureOutput = async ({
|
|
1048
|
+
worktree,
|
|
1049
|
+
config,
|
|
1050
|
+
publishedSkillsResult,
|
|
1051
|
+
loginBootstrapSnapshot
|
|
1052
|
+
}) => ({
|
|
1053
|
+
output: await formatStatusOutput(worktree, config, publishedSkillsResult, loginBootstrapSnapshot),
|
|
1054
|
+
metadata: {
|
|
1055
|
+
status: publishedSkillsResult.fetchResult.status,
|
|
1056
|
+
...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
|
|
1057
|
+
source: publishedSkillsResult.fetchResult.source
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
const startLoginFlow = async signal => {
|
|
1061
|
+
const discovery = await fetchOidcDiscoveryDocument(signal);
|
|
1062
|
+
const codeVerifier = createRandomBase64Url(64);
|
|
1063
|
+
const expectedState = createRandomBase64Url(32);
|
|
1064
|
+
const codeChallenge = createCodeChallenge(codeVerifier);
|
|
1065
|
+
const expiresAt = new Date(Date.now() + LOGIN_TIMEOUT_MS).toISOString();
|
|
1066
|
+
const {
|
|
1067
|
+
callbackPromise,
|
|
1068
|
+
close
|
|
1069
|
+
} = await startLocalCallbackServer({
|
|
1070
|
+
expectedState,
|
|
1071
|
+
signal
|
|
1072
|
+
});
|
|
1073
|
+
const browserUrl = new URL(discovery.authorization_endpoint);
|
|
1074
|
+
browserUrl.searchParams.set('client_id', OIDC_CLIENT_ID);
|
|
1075
|
+
browserUrl.searchParams.set('response_type', 'code');
|
|
1076
|
+
browserUrl.searchParams.set('redirect_uri', OIDC_CALLBACK_URL);
|
|
1077
|
+
browserUrl.searchParams.set('response_mode', 'query');
|
|
1078
|
+
browserUrl.searchParams.set('scope', OIDC_SCOPES.join(' '));
|
|
1079
|
+
browserUrl.searchParams.set('code_challenge', codeChallenge);
|
|
1080
|
+
browserUrl.searchParams.set('code_challenge_method', 'S256');
|
|
1081
|
+
browserUrl.searchParams.set('state', expectedState);
|
|
1082
|
+
return {
|
|
1083
|
+
browserUrl: browserUrl.toString(),
|
|
1084
|
+
expiresAt,
|
|
1085
|
+
codeVerifier,
|
|
1086
|
+
expectedState,
|
|
1087
|
+
callbackPromise,
|
|
1088
|
+
closeCallbackServer: close
|
|
1089
|
+
};
|
|
1090
|
+
};
|
|
1091
|
+
const createPluginSession = async ({
|
|
1092
|
+
code,
|
|
1093
|
+
codeVerifier,
|
|
1094
|
+
redirectUri,
|
|
1095
|
+
config,
|
|
1096
|
+
signal
|
|
1097
|
+
}) => {
|
|
1098
|
+
const response = await fetch(config.authSessionUrl, {
|
|
1099
|
+
method: 'POST',
|
|
1100
|
+
headers: {
|
|
1101
|
+
'content-type': 'application/json'
|
|
1102
|
+
},
|
|
1103
|
+
body: JSON.stringify({
|
|
1104
|
+
code,
|
|
1105
|
+
codeVerifier,
|
|
1106
|
+
redirectUri
|
|
1107
|
+
}),
|
|
1108
|
+
signal
|
|
1109
|
+
});
|
|
1110
|
+
const payload = await response.json().catch(() => null);
|
|
1111
|
+
if (!response.ok) {
|
|
1112
|
+
throw new Error(getMessageFromUnknownPayload(payload) ?? `Plugin session exchange failed with HTTP ${response.status}.`);
|
|
1113
|
+
}
|
|
1114
|
+
if (!payload || !('success' in payload) || payload.success !== true) {
|
|
1115
|
+
throw new Error('Plugin session exchange returned an unexpected payload.');
|
|
1116
|
+
}
|
|
1117
|
+
return payload.session;
|
|
1118
|
+
};
|
|
1119
|
+
const emitPresenceEvent = async ({
|
|
1120
|
+
config,
|
|
1121
|
+
authState,
|
|
1122
|
+
event,
|
|
1123
|
+
workspacePath
|
|
1124
|
+
}) => {
|
|
1125
|
+
for (let attempt = 1; attempt <= PRESENCE_EVENT_MAX_ATTEMPTS; attempt += 1) {
|
|
1126
|
+
try {
|
|
1127
|
+
const response = await fetch(config.presenceUrl, {
|
|
1003
1128
|
method: 'POST',
|
|
1004
1129
|
headers: {
|
|
1005
|
-
|
|
1130
|
+
'content-type': 'application/json',
|
|
1131
|
+
authorization: `Bearer ${authState.sessionToken}`
|
|
1006
1132
|
},
|
|
1007
1133
|
body: JSON.stringify({
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1134
|
+
event,
|
|
1135
|
+
occurredAt: new Date().toISOString(),
|
|
1136
|
+
workspacePath
|
|
1011
1137
|
}),
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
throw new Error('Plugin session exchange returned an unexpected payload.');
|
|
1020
|
-
}
|
|
1021
|
-
return payload.session;
|
|
1022
|
-
};
|
|
1023
|
-
const emitPresenceEvent = async ({ config, authState, event, workspacePath, }) => {
|
|
1024
|
-
for (let attempt = 1; attempt <= PRESENCE_EVENT_MAX_ATTEMPTS; attempt += 1) {
|
|
1025
|
-
try {
|
|
1026
|
-
const response = await fetch(config.presenceUrl, {
|
|
1027
|
-
method: 'POST',
|
|
1028
|
-
headers: {
|
|
1029
|
-
'content-type': 'application/json',
|
|
1030
|
-
authorization: `Bearer ${authState.sessionToken}`,
|
|
1031
|
-
},
|
|
1032
|
-
body: JSON.stringify({
|
|
1033
|
-
event,
|
|
1034
|
-
occurredAt: new Date().toISOString(),
|
|
1035
|
-
workspacePath,
|
|
1036
|
-
}),
|
|
1037
|
-
keepalive: event === 'STOP',
|
|
1038
|
-
signal: AbortSignal.timeout(PRESENCE_EVENT_TIMEOUT_MS),
|
|
1039
|
-
});
|
|
1040
|
-
if (response.ok)
|
|
1041
|
-
return;
|
|
1042
|
-
if (!shouldRetryPresenceEvent(response.status) || attempt === PRESENCE_EVENT_MAX_ATTEMPTS)
|
|
1043
|
-
return;
|
|
1044
|
-
}
|
|
1045
|
-
catch {
|
|
1046
|
-
if (attempt === PRESENCE_EVENT_MAX_ATTEMPTS)
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
await wait(PRESENCE_EVENT_RETRY_DELAY_MS * attempt);
|
|
1138
|
+
keepalive: event === 'STOP',
|
|
1139
|
+
signal: AbortSignal.timeout(PRESENCE_EVENT_TIMEOUT_MS)
|
|
1140
|
+
});
|
|
1141
|
+
if (response.ok) return;
|
|
1142
|
+
if (!shouldRetryPresenceEvent(response.status) || attempt === PRESENCE_EVENT_MAX_ATTEMPTS) return;
|
|
1143
|
+
} catch {
|
|
1144
|
+
if (attempt === PRESENCE_EVENT_MAX_ATTEMPTS) return;
|
|
1050
1145
|
}
|
|
1146
|
+
await wait(PRESENCE_EVENT_RETRY_DELAY_MS * attempt);
|
|
1147
|
+
}
|
|
1051
1148
|
};
|
|
1052
|
-
const emitPluginActionEvent = async ({
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1149
|
+
const emitPluginActionEvent = async ({
|
|
1150
|
+
config,
|
|
1151
|
+
authState,
|
|
1152
|
+
event,
|
|
1153
|
+
workspacePath,
|
|
1154
|
+
directoryPath
|
|
1155
|
+
}) => {
|
|
1156
|
+
if (!authState) return;
|
|
1157
|
+
try {
|
|
1158
|
+
await fetch(config.actionsUrl, {
|
|
1159
|
+
method: 'POST',
|
|
1160
|
+
headers: {
|
|
1161
|
+
'content-type': 'application/json',
|
|
1162
|
+
authorization: `Bearer ${authState.sessionToken}`
|
|
1163
|
+
},
|
|
1164
|
+
body: JSON.stringify({
|
|
1165
|
+
event,
|
|
1166
|
+
occurredAt: new Date().toISOString(),
|
|
1167
|
+
workspacePath,
|
|
1168
|
+
directoryPath
|
|
1169
|
+
}),
|
|
1170
|
+
keepalive: event === 'STOP',
|
|
1171
|
+
signal: AbortSignal.timeout(PRESENCE_EVENT_TIMEOUT_MS)
|
|
1172
|
+
});
|
|
1173
|
+
} catch {
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1075
1176
|
};
|
|
1076
|
-
const openBrowser = async
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
}
|
|
1082
|
-
if (process.platform === 'win32') {
|
|
1083
|
-
await execFileAsync('cmd', ['/c', 'start', '', url]);
|
|
1084
|
-
return null;
|
|
1085
|
-
}
|
|
1086
|
-
await execFileAsync('xdg-open', [url]);
|
|
1087
|
-
return null;
|
|
1177
|
+
const openBrowser = async url => {
|
|
1178
|
+
try {
|
|
1179
|
+
if (process.platform === 'darwin') {
|
|
1180
|
+
await execFileAsync('open', [url]);
|
|
1181
|
+
return null;
|
|
1088
1182
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1183
|
+
if (process.platform === 'win32') {
|
|
1184
|
+
await execFileAsync('cmd', ['/c', 'start', '', url]);
|
|
1185
|
+
return null;
|
|
1091
1186
|
}
|
|
1187
|
+
await execFileAsync('xdg-open', [url]);
|
|
1188
|
+
return null;
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
return error instanceof Error ? error.message : 'Failed to open browser automatically';
|
|
1191
|
+
}
|
|
1092
1192
|
};
|
|
1093
1193
|
const normalizeDirectoryArg = (contextDirectory, directory) => {
|
|
1094
|
-
|
|
1194
|
+
return normalizeAbsolutePath(directory ? path.resolve(contextDirectory, directory) : contextDirectory);
|
|
1095
1195
|
};
|
|
1096
1196
|
const getDetailCacheKey = (workspaceResolution, skillVersionId) => {
|
|
1097
|
-
|
|
1098
|
-
};
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1197
|
+
return JSON.stringify([workspaceResolution.cacheKey, skillVersionId]);
|
|
1198
|
+
};
|
|
1199
|
+
const getDetailInflightKey = (workspaceResolution, skillVersionId, purpose) => {
|
|
1200
|
+
return JSON.stringify([workspaceResolution.cacheKey, skillVersionId, purpose]);
|
|
1201
|
+
};
|
|
1202
|
+
const OpencodeWizardSkillsPlugin = async input => {
|
|
1203
|
+
const {
|
|
1204
|
+
tool
|
|
1205
|
+
} = await importOpencodePluginModule('@opencode-ai/plugin');
|
|
1206
|
+
const config = await resolveConfig(input.worktree);
|
|
1207
|
+
const workspacePath = normalizeAbsolutePath(input.worktree);
|
|
1208
|
+
const cache = new Map();
|
|
1209
|
+
const catalogInflight = new Map();
|
|
1210
|
+
const detailCache = new Map();
|
|
1211
|
+
const detailInflight = new Map();
|
|
1212
|
+
const authStateFile = path.resolve(input.worktree, config.authStatePath);
|
|
1213
|
+
const initialAuthState = await resolveStoredAuthState(input.worktree, config);
|
|
1214
|
+
const loginBootstrap = {
|
|
1215
|
+
promise: null,
|
|
1216
|
+
snapshot: createIdleLoginBootstrapSnapshot()
|
|
1217
|
+
};
|
|
1218
|
+
let lastAuthenticatedAuthState = initialAuthState;
|
|
1219
|
+
let didEmitStart = false;
|
|
1220
|
+
let didScheduleStop = false;
|
|
1221
|
+
let presenceStartPromise = null;
|
|
1222
|
+
let presenceStopPromise = null;
|
|
1223
|
+
let lastInteractiveDirectoryPath = null;
|
|
1224
|
+
const resolveActionAuthState = async () => {
|
|
1225
|
+
const storedAuthState = await resolveStoredAuthState(input.worktree, config);
|
|
1226
|
+
if (storedAuthState) return storedAuthState;
|
|
1227
|
+
return lastAuthenticatedAuthState;
|
|
1228
|
+
};
|
|
1229
|
+
const emitActionEventForCurrentSession = async ({
|
|
1230
|
+
event,
|
|
1231
|
+
authState,
|
|
1232
|
+
directoryPath
|
|
1233
|
+
}) => {
|
|
1234
|
+
await emitPluginActionEvent({
|
|
1235
|
+
config,
|
|
1236
|
+
authState: authState ?? (await resolveActionAuthState()),
|
|
1237
|
+
event,
|
|
1238
|
+
workspacePath,
|
|
1239
|
+
directoryPath
|
|
1240
|
+
});
|
|
1241
|
+
};
|
|
1242
|
+
const schedulePresenceStart = authState => {
|
|
1243
|
+
lastAuthenticatedAuthState = authState;
|
|
1244
|
+
if (didEmitStart) {
|
|
1245
|
+
return presenceStartPromise ?? Promise.resolve();
|
|
1246
|
+
}
|
|
1247
|
+
didEmitStart = true;
|
|
1248
|
+
presenceStartPromise = Promise.all([emitPresenceEvent({
|
|
1249
|
+
config,
|
|
1250
|
+
authState,
|
|
1251
|
+
event: 'START',
|
|
1252
|
+
workspacePath
|
|
1253
|
+
}), emitActionEventForCurrentSession({
|
|
1254
|
+
event: 'START',
|
|
1255
|
+
authState,
|
|
1256
|
+
directoryPath: lastInteractiveDirectoryPath ?? undefined
|
|
1257
|
+
})]).then(() => undefined);
|
|
1258
|
+
return presenceStartPromise;
|
|
1259
|
+
};
|
|
1260
|
+
const schedulePresenceStop = () => {
|
|
1261
|
+
if (didScheduleStop) {
|
|
1262
|
+
return presenceStopPromise ?? Promise.resolve();
|
|
1263
|
+
}
|
|
1264
|
+
didScheduleStop = true;
|
|
1265
|
+
if (!didEmitStart || !lastAuthenticatedAuthState) {
|
|
1266
|
+
presenceStopPromise = Promise.resolve();
|
|
1267
|
+
return presenceStopPromise;
|
|
1268
|
+
}
|
|
1269
|
+
presenceStopPromise = (async () => {
|
|
1270
|
+
await presenceStartPromise?.catch(() => undefined);
|
|
1271
|
+
await Promise.all([emitPresenceEvent({
|
|
1272
|
+
config,
|
|
1273
|
+
authState: lastAuthenticatedAuthState,
|
|
1274
|
+
event: 'STOP',
|
|
1275
|
+
workspacePath
|
|
1276
|
+
}), emitActionEventForCurrentSession({
|
|
1277
|
+
event: 'STOP',
|
|
1278
|
+
authState: lastAuthenticatedAuthState,
|
|
1279
|
+
directoryPath: lastInteractiveDirectoryPath ?? undefined
|
|
1280
|
+
})]);
|
|
1281
|
+
})();
|
|
1282
|
+
return presenceStopPromise;
|
|
1283
|
+
};
|
|
1284
|
+
const scheduleInteractivePresenceStart = async () => {
|
|
1285
|
+
const authState = await resolveStoredAuthState(input.worktree, config);
|
|
1286
|
+
if (!authState) return;
|
|
1287
|
+
await schedulePresenceStart(authState);
|
|
1288
|
+
};
|
|
1289
|
+
process.once('beforeExit', () => {
|
|
1290
|
+
void schedulePresenceStop();
|
|
1291
|
+
});
|
|
1292
|
+
for (const shutdownSignal of PRESENCE_SHUTDOWN_SIGNALS) {
|
|
1293
|
+
try {
|
|
1294
|
+
process.once(shutdownSignal, () => {
|
|
1295
|
+
void schedulePresenceStop().finally(() => {
|
|
1296
|
+
process.exit(PRESENCE_SIGNAL_EXIT_CODES[shutdownSignal]);
|
|
1131
1297
|
});
|
|
1298
|
+
});
|
|
1299
|
+
} catch {
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
const clearPublishedSkillState = () => {
|
|
1304
|
+
cache.clear();
|
|
1305
|
+
catalogInflight.clear();
|
|
1306
|
+
detailCache.clear();
|
|
1307
|
+
detailInflight.clear();
|
|
1308
|
+
};
|
|
1309
|
+
const persistAuthState = async session => {
|
|
1310
|
+
const authState = toAuthState(session);
|
|
1311
|
+
await writeAuthState(authStateFile, authState);
|
|
1312
|
+
clearPublishedSkillState();
|
|
1313
|
+
return authState;
|
|
1314
|
+
};
|
|
1315
|
+
const startLoginCompletion = trigger => {
|
|
1316
|
+
if (loginBootstrap.promise) {
|
|
1317
|
+
return loginBootstrap.promise;
|
|
1318
|
+
}
|
|
1319
|
+
const startedAt = new Date().toISOString();
|
|
1320
|
+
loginBootstrap.snapshot = {
|
|
1321
|
+
status: 'starting',
|
|
1322
|
+
trigger,
|
|
1323
|
+
startedAt,
|
|
1324
|
+
expiresAt: null,
|
|
1325
|
+
browserUrl: null,
|
|
1326
|
+
browserOpenError: null,
|
|
1327
|
+
email: null,
|
|
1328
|
+
message: null
|
|
1132
1329
|
};
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
]).then(() => undefined);
|
|
1152
|
-
return presenceStartPromise;
|
|
1153
|
-
};
|
|
1154
|
-
const schedulePresenceStop = () => {
|
|
1155
|
-
if (didScheduleStop) {
|
|
1156
|
-
return presenceStopPromise ?? Promise.resolve();
|
|
1330
|
+
const loginPromise = (async () => {
|
|
1331
|
+
const loginSignal = AbortSignal.timeout(LOGIN_TIMEOUT_MS);
|
|
1332
|
+
const loginStart = await startLoginFlow(loginSignal);
|
|
1333
|
+
const browserOpenError = await openBrowser(loginStart.browserUrl);
|
|
1334
|
+
loginBootstrap.snapshot = {
|
|
1335
|
+
status: 'pending',
|
|
1336
|
+
trigger,
|
|
1337
|
+
startedAt,
|
|
1338
|
+
expiresAt: loginStart.expiresAt,
|
|
1339
|
+
browserUrl: loginStart.browserUrl,
|
|
1340
|
+
browserOpenError,
|
|
1341
|
+
email: null,
|
|
1342
|
+
message: browserOpenError ? `Automatic browser open failed. Open ${loginStart.browserUrl} manually.` : 'Browser login started for published skill fetch.'
|
|
1343
|
+
};
|
|
1344
|
+
try {
|
|
1345
|
+
const callbackPayload = await loginStart.callbackPromise;
|
|
1346
|
+
if (callbackPayload.status === 'error') {
|
|
1347
|
+
throw new Error(callbackPayload.message);
|
|
1157
1348
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
presenceStopPromise = Promise.resolve();
|
|
1161
|
-
return presenceStopPromise;
|
|
1349
|
+
if (callbackPayload.state !== loginStart.expectedState) {
|
|
1350
|
+
throw new Error('OAuth callback state did not match the original login request.');
|
|
1162
1351
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1352
|
+
loginBootstrap.snapshot = {
|
|
1353
|
+
status: 'pending',
|
|
1354
|
+
trigger,
|
|
1355
|
+
startedAt,
|
|
1356
|
+
expiresAt: loginStart.expiresAt,
|
|
1357
|
+
browserUrl: loginStart.browserUrl,
|
|
1358
|
+
browserOpenError,
|
|
1359
|
+
email: null,
|
|
1360
|
+
message: 'OAuth callback received. Finalizing backend session exchange.'
|
|
1361
|
+
};
|
|
1362
|
+
const pluginSession = await createPluginSession({
|
|
1363
|
+
code: callbackPayload.code,
|
|
1364
|
+
codeVerifier: loginStart.codeVerifier,
|
|
1365
|
+
redirectUri: OIDC_CALLBACK_URL,
|
|
1366
|
+
config,
|
|
1367
|
+
signal: loginSignal
|
|
1368
|
+
});
|
|
1369
|
+
const authState = await persistAuthState(pluginSession);
|
|
1370
|
+
await emitActionEventForCurrentSession({
|
|
1371
|
+
event: 'LOGIN_SUCCESS',
|
|
1372
|
+
authState,
|
|
1373
|
+
directoryPath: lastInteractiveDirectoryPath ?? undefined
|
|
1374
|
+
});
|
|
1375
|
+
loginBootstrap.snapshot = {
|
|
1376
|
+
status: 'authenticated',
|
|
1377
|
+
trigger,
|
|
1378
|
+
startedAt,
|
|
1379
|
+
expiresAt: authState.expiresAt,
|
|
1380
|
+
browserUrl: loginStart.browserUrl,
|
|
1381
|
+
browserOpenError,
|
|
1382
|
+
email: authState.email,
|
|
1383
|
+
message: 'Browser login completed successfully for published skill fetch.'
|
|
1384
|
+
};
|
|
1385
|
+
return authState;
|
|
1386
|
+
} catch (error) {
|
|
1387
|
+
await emitActionEventForCurrentSession({
|
|
1388
|
+
event: 'LOGIN_FAILED',
|
|
1389
|
+
directoryPath: lastInteractiveDirectoryPath ?? undefined
|
|
1390
|
+
});
|
|
1391
|
+
loginBootstrap.snapshot = {
|
|
1392
|
+
status: 'failed',
|
|
1393
|
+
trigger,
|
|
1394
|
+
startedAt,
|
|
1395
|
+
expiresAt: loginBootstrap.snapshot.expiresAt,
|
|
1396
|
+
browserUrl: loginBootstrap.snapshot.browserUrl,
|
|
1397
|
+
browserOpenError: loginBootstrap.snapshot.browserOpenError,
|
|
1398
|
+
email: null,
|
|
1399
|
+
message: error instanceof Error ? error.message : 'Browser login failed.'
|
|
1400
|
+
};
|
|
1401
|
+
throw error;
|
|
1402
|
+
} finally {
|
|
1403
|
+
await loginStart.closeCallbackServer().catch(() => undefined);
|
|
1404
|
+
loginBootstrap.promise = null;
|
|
1405
|
+
}
|
|
1406
|
+
})();
|
|
1407
|
+
loginBootstrap.promise = loginPromise;
|
|
1408
|
+
return loginPromise;
|
|
1409
|
+
};
|
|
1410
|
+
const loadPublishedSkillCatalog = async ({
|
|
1411
|
+
directory,
|
|
1412
|
+
useCache,
|
|
1413
|
+
signal
|
|
1414
|
+
}) => {
|
|
1415
|
+
const workspaceResolution = await resolveWorkspace({
|
|
1416
|
+
config,
|
|
1417
|
+
directory
|
|
1189
1418
|
});
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1419
|
+
const directoryPath = workspaceResolution.directoryPath;
|
|
1420
|
+
const cacheKey = workspaceResolution.cacheKey;
|
|
1421
|
+
const cached = cache.get(cacheKey);
|
|
1422
|
+
if (useCache && cached && cached.expiresAt > Date.now()) {
|
|
1423
|
+
return {
|
|
1424
|
+
directoryPath,
|
|
1425
|
+
workspaceResolution,
|
|
1426
|
+
fetchResult: {
|
|
1427
|
+
...cached.result,
|
|
1428
|
+
source: 'cache'
|
|
1200
1429
|
}
|
|
1430
|
+
};
|
|
1201
1431
|
}
|
|
1202
|
-
const
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1432
|
+
const inflight = catalogInflight.get(cacheKey);
|
|
1433
|
+
if (inflight) {
|
|
1434
|
+
return inflight;
|
|
1435
|
+
}
|
|
1436
|
+
const requestPromise = (async () => {
|
|
1437
|
+
const fetchResult = await fetchPublishedSkillsCatalog(input.worktree, config, workspaceResolution, signal, clearPublishedSkillState);
|
|
1438
|
+
cache.set(cacheKey, {
|
|
1439
|
+
result: fetchResult,
|
|
1440
|
+
expiresAt: Date.now() + CACHE_TTL_MS
|
|
1441
|
+
});
|
|
1442
|
+
return {
|
|
1443
|
+
directoryPath,
|
|
1444
|
+
workspaceResolution,
|
|
1445
|
+
fetchResult
|
|
1446
|
+
};
|
|
1447
|
+
})();
|
|
1448
|
+
catalogInflight.set(cacheKey, requestPromise);
|
|
1449
|
+
try {
|
|
1450
|
+
return await requestPromise;
|
|
1451
|
+
} finally {
|
|
1452
|
+
catalogInflight.delete(cacheKey);
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
const loadPublishedSkillDetail = async ({
|
|
1456
|
+
workspaceResolution,
|
|
1457
|
+
item,
|
|
1458
|
+
signal,
|
|
1459
|
+
useCache,
|
|
1460
|
+
purpose
|
|
1461
|
+
}) => {
|
|
1462
|
+
const directoryPath = workspaceResolution.directoryPath;
|
|
1463
|
+
const cacheKey = getDetailCacheKey(workspaceResolution, item.skillVersion.id);
|
|
1464
|
+
const inflightKey = getDetailInflightKey(workspaceResolution, item.skillVersion.id, purpose);
|
|
1465
|
+
const cached = detailCache.get(cacheKey);
|
|
1466
|
+
if (purpose === 'SYSTEM_CONTEXT' && useCache && cached && cached.expiresAt > Date.now()) {
|
|
1467
|
+
return {
|
|
1468
|
+
ok: true,
|
|
1469
|
+
detail: toPublishedSkillDetail({
|
|
1470
|
+
...item,
|
|
1471
|
+
publishedArtifact: cached.artifact
|
|
1472
|
+
})
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
const inflight = detailInflight.get(inflightKey);
|
|
1476
|
+
if (inflight) {
|
|
1477
|
+
return inflight;
|
|
1478
|
+
}
|
|
1479
|
+
const requestPromise = (async () => {
|
|
1480
|
+
const detailResult = await fetchPublishedSkillDetail({
|
|
1481
|
+
worktree: input.worktree,
|
|
1482
|
+
config,
|
|
1483
|
+
resolution: workspaceResolution,
|
|
1484
|
+
skillVersionId: item.skillVersion.id,
|
|
1485
|
+
signal,
|
|
1486
|
+
onAuthStateChanged: clearPublishedSkillState,
|
|
1487
|
+
purpose
|
|
1488
|
+
});
|
|
1489
|
+
if (!detailResult.ok) {
|
|
1490
|
+
return {
|
|
1491
|
+
ok: false,
|
|
1492
|
+
status: detailResult.result.status,
|
|
1493
|
+
output: JSON.stringify({
|
|
1494
|
+
pluginId: PLUGIN_ID,
|
|
1495
|
+
runtimeMode: 'tool_fetch_only',
|
|
1496
|
+
status: detailResult.result.status,
|
|
1497
|
+
requestedDirectoryPath: directoryPath,
|
|
1498
|
+
workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
|
|
1499
|
+
requestedSkillVersionId: item.skillVersion.id,
|
|
1500
|
+
message: detailResult.result.message,
|
|
1501
|
+
fetchedAt: detailResult.result.fetchedAt,
|
|
1502
|
+
source: detailResult.result.source
|
|
1503
|
+
}, null, 2),
|
|
1504
|
+
metadata: {
|
|
1505
|
+
status: detailResult.result.status,
|
|
1506
|
+
...toWorkspaceResolutionMetadata(workspaceResolution),
|
|
1507
|
+
source: detailResult.result.source
|
|
1508
|
+
}
|
|
1228
1509
|
};
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
event: 'LOGIN_FAILED',
|
|
1289
|
-
directoryPath: lastInteractiveDirectoryPath ?? undefined,
|
|
1290
|
-
});
|
|
1291
|
-
loginBootstrap.snapshot = {
|
|
1292
|
-
status: 'failed',
|
|
1293
|
-
trigger,
|
|
1294
|
-
startedAt,
|
|
1295
|
-
expiresAt: loginBootstrap.snapshot.expiresAt,
|
|
1296
|
-
browserUrl: loginBootstrap.snapshot.browserUrl,
|
|
1297
|
-
browserOpenError: loginBootstrap.snapshot.browserOpenError,
|
|
1298
|
-
email: null,
|
|
1299
|
-
message: error instanceof Error ? error.message : 'Browser login failed.',
|
|
1300
|
-
};
|
|
1301
|
-
throw error;
|
|
1302
|
-
}
|
|
1303
|
-
finally {
|
|
1304
|
-
await loginStart.closeCallbackServer().catch(() => undefined);
|
|
1305
|
-
loginBootstrap.promise = null;
|
|
1306
|
-
}
|
|
1307
|
-
})();
|
|
1308
|
-
loginBootstrap.promise = loginPromise;
|
|
1309
|
-
return loginPromise;
|
|
1510
|
+
}
|
|
1511
|
+
detailCache.set(cacheKey, {
|
|
1512
|
+
artifact: detailResult.artifact,
|
|
1513
|
+
expiresAt: Date.now() + CACHE_TTL_MS
|
|
1514
|
+
});
|
|
1515
|
+
return {
|
|
1516
|
+
ok: true,
|
|
1517
|
+
detail: toPublishedSkillDetail({
|
|
1518
|
+
...item,
|
|
1519
|
+
publishedArtifact: detailResult.artifact
|
|
1520
|
+
})
|
|
1521
|
+
};
|
|
1522
|
+
})();
|
|
1523
|
+
detailInflight.set(inflightKey, requestPromise);
|
|
1524
|
+
try {
|
|
1525
|
+
return await requestPromise;
|
|
1526
|
+
} finally {
|
|
1527
|
+
detailInflight.delete(inflightKey);
|
|
1528
|
+
}
|
|
1529
|
+
};
|
|
1530
|
+
const loadSystemNoteDetails = async ({
|
|
1531
|
+
publishedSkillsResult,
|
|
1532
|
+
signal
|
|
1533
|
+
}) => {
|
|
1534
|
+
if (!publishedSkillsResult.fetchResult.ok) return [];
|
|
1535
|
+
const prioritizedItems = [...publishedSkillsResult.fetchResult.payload.skills].sort((left, right) => {
|
|
1536
|
+
const leftSummary = toPublishedSkillSummary(left);
|
|
1537
|
+
const rightSummary = toPublishedSkillSummary(right);
|
|
1538
|
+
if (leftSummary.contextKind === rightSummary.contextKind) return formatSkillLabel(left).localeCompare(formatSkillLabel(right));
|
|
1539
|
+
if (leftSummary.contextKind === 'global') return -1;
|
|
1540
|
+
if (rightSummary.contextKind === 'global') return 1;
|
|
1541
|
+
return 0;
|
|
1542
|
+
});
|
|
1543
|
+
const detailResults = await Promise.all(prioritizedItems.slice(0, SYSTEM_NOTE_DETAIL_LIMIT).map(item => loadPublishedSkillDetail({
|
|
1544
|
+
workspaceResolution: publishedSkillsResult.workspaceResolution,
|
|
1545
|
+
item,
|
|
1546
|
+
signal,
|
|
1547
|
+
useCache: true,
|
|
1548
|
+
purpose: 'SYSTEM_CONTEXT'
|
|
1549
|
+
})));
|
|
1550
|
+
return detailResults.reduce((details, result) => {
|
|
1551
|
+
if (!result.ok) return details;
|
|
1552
|
+
details.push(result.detail);
|
|
1553
|
+
return details;
|
|
1554
|
+
}, []);
|
|
1555
|
+
};
|
|
1556
|
+
const executePublishedSkillsFetchTool = async ({
|
|
1557
|
+
args,
|
|
1558
|
+
context
|
|
1559
|
+
}) => {
|
|
1560
|
+
const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
|
|
1561
|
+
const requestedSkills = parseRequestedSkillArgs(args);
|
|
1562
|
+
const fetchActionDirectoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
|
|
1563
|
+
lastInteractiveDirectoryPath = fetchActionDirectoryPath;
|
|
1564
|
+
const emitFetchOutcome = async event => {
|
|
1565
|
+
await emitActionEventForCurrentSession({
|
|
1566
|
+
event,
|
|
1567
|
+
directoryPath: fetchActionDirectoryPath
|
|
1568
|
+
});
|
|
1310
1569
|
};
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1570
|
+
let publishedSkillsResult = await loadPublishedSkillCatalog({
|
|
1571
|
+
directory: requestedDirectory,
|
|
1572
|
+
useCache: !args.refresh,
|
|
1573
|
+
signal: context.abort
|
|
1574
|
+
});
|
|
1575
|
+
if (publishedSkillsResult.fetchResult.ok) {
|
|
1576
|
+
await scheduleInteractivePresenceStart();
|
|
1577
|
+
}
|
|
1578
|
+
if (!publishedSkillsResult.fetchResult.ok && publishedSkillsResult.fetchResult.status === 'missing_auth') {
|
|
1579
|
+
try {
|
|
1580
|
+
await startLoginCompletion('fetch').then(async authState => {
|
|
1581
|
+
await schedulePresenceStart(authState);
|
|
1315
1582
|
});
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
return {
|
|
1321
|
-
directoryPath,
|
|
1322
|
-
workspaceResolution,
|
|
1323
|
-
fetchResult: {
|
|
1324
|
-
...cached.result,
|
|
1325
|
-
source: 'cache',
|
|
1326
|
-
},
|
|
1327
|
-
};
|
|
1328
|
-
}
|
|
1329
|
-
const inflight = catalogInflight.get(cacheKey);
|
|
1330
|
-
if (inflight) {
|
|
1331
|
-
return inflight;
|
|
1332
|
-
}
|
|
1333
|
-
const requestPromise = (async () => {
|
|
1334
|
-
const fetchResult = await fetchPublishedSkillsCatalog(input.worktree, config, workspaceResolution, signal, clearPublishedSkillState);
|
|
1335
|
-
cache.set(cacheKey, {
|
|
1336
|
-
result: fetchResult,
|
|
1337
|
-
expiresAt: Date.now() + CACHE_TTL_MS,
|
|
1338
|
-
});
|
|
1339
|
-
return {
|
|
1340
|
-
directoryPath,
|
|
1341
|
-
workspaceResolution,
|
|
1342
|
-
fetchResult,
|
|
1343
|
-
};
|
|
1344
|
-
})();
|
|
1345
|
-
catalogInflight.set(cacheKey, requestPromise);
|
|
1346
|
-
try {
|
|
1347
|
-
return await requestPromise;
|
|
1348
|
-
}
|
|
1349
|
-
finally {
|
|
1350
|
-
catalogInflight.delete(cacheKey);
|
|
1351
|
-
}
|
|
1352
|
-
};
|
|
1353
|
-
const loadPublishedSkillDetail = async ({ workspaceResolution, item, signal, useCache, }) => {
|
|
1354
|
-
const directoryPath = workspaceResolution.directoryPath;
|
|
1355
|
-
const cacheKey = getDetailCacheKey(workspaceResolution, item.skillVersion.id);
|
|
1356
|
-
const cached = detailCache.get(cacheKey);
|
|
1357
|
-
if (useCache && cached && cached.expiresAt > Date.now()) {
|
|
1358
|
-
return {
|
|
1359
|
-
ok: true,
|
|
1360
|
-
detail: toPublishedSkillDetail({
|
|
1361
|
-
...item,
|
|
1362
|
-
publishedArtifact: cached.artifact,
|
|
1363
|
-
}),
|
|
1364
|
-
};
|
|
1365
|
-
}
|
|
1366
|
-
const inflight = detailInflight.get(cacheKey);
|
|
1367
|
-
if (inflight) {
|
|
1368
|
-
return inflight;
|
|
1369
|
-
}
|
|
1370
|
-
const requestPromise = (async () => {
|
|
1371
|
-
const detailResult = await fetchPublishedSkillDetail({
|
|
1372
|
-
worktree: input.worktree,
|
|
1373
|
-
config,
|
|
1374
|
-
resolution: workspaceResolution,
|
|
1375
|
-
skillVersionId: item.skillVersion.id,
|
|
1376
|
-
signal,
|
|
1377
|
-
onAuthStateChanged: clearPublishedSkillState,
|
|
1378
|
-
});
|
|
1379
|
-
if (!detailResult.ok) {
|
|
1380
|
-
return {
|
|
1381
|
-
ok: false,
|
|
1382
|
-
status: detailResult.result.status,
|
|
1383
|
-
output: JSON.stringify({
|
|
1384
|
-
pluginId: PLUGIN_ID,
|
|
1385
|
-
runtimeMode: 'tool_fetch_only',
|
|
1386
|
-
status: detailResult.result.status,
|
|
1387
|
-
requestedDirectoryPath: directoryPath,
|
|
1388
|
-
workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
|
|
1389
|
-
requestedSkillVersionId: item.skillVersion.id,
|
|
1390
|
-
message: detailResult.result.message,
|
|
1391
|
-
fetchedAt: detailResult.result.fetchedAt,
|
|
1392
|
-
source: detailResult.result.source,
|
|
1393
|
-
}, null, 2),
|
|
1394
|
-
metadata: {
|
|
1395
|
-
status: detailResult.result.status,
|
|
1396
|
-
...toWorkspaceResolutionMetadata(workspaceResolution),
|
|
1397
|
-
source: detailResult.result.source,
|
|
1398
|
-
},
|
|
1399
|
-
};
|
|
1400
|
-
}
|
|
1401
|
-
detailCache.set(cacheKey, {
|
|
1402
|
-
artifact: detailResult.artifact,
|
|
1403
|
-
expiresAt: Date.now() + CACHE_TTL_MS,
|
|
1404
|
-
});
|
|
1405
|
-
return {
|
|
1406
|
-
ok: true,
|
|
1407
|
-
detail: toPublishedSkillDetail({
|
|
1408
|
-
...item,
|
|
1409
|
-
publishedArtifact: detailResult.artifact,
|
|
1410
|
-
}),
|
|
1411
|
-
};
|
|
1412
|
-
})();
|
|
1413
|
-
detailInflight.set(cacheKey, requestPromise);
|
|
1414
|
-
try {
|
|
1415
|
-
return await requestPromise;
|
|
1416
|
-
}
|
|
1417
|
-
finally {
|
|
1418
|
-
detailInflight.delete(cacheKey);
|
|
1419
|
-
}
|
|
1420
|
-
};
|
|
1421
|
-
const executePublishedSkillsFetchTool = async ({ args, context, }) => {
|
|
1422
|
-
const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
|
|
1423
|
-
const requestedSkills = parseRequestedSkillArgs(args);
|
|
1424
|
-
if (requestedSkills.length === 0) {
|
|
1425
|
-
return {
|
|
1426
|
-
output: JSON.stringify({
|
|
1427
|
-
pluginId: PLUGIN_ID,
|
|
1428
|
-
runtimeMode: 'tool_fetch_only',
|
|
1429
|
-
status: 'invalid_request',
|
|
1430
|
-
message: 'Provide `skill` for one request or `skills` for one or more comma/newline-separated requests. Passive/system context lists the published skills available for the current scope.',
|
|
1431
|
-
}, null, 2),
|
|
1432
|
-
metadata: {
|
|
1433
|
-
status: 'invalid_request',
|
|
1434
|
-
},
|
|
1435
|
-
};
|
|
1436
|
-
}
|
|
1437
|
-
const fetchActionDirectoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
|
|
1438
|
-
lastInteractiveDirectoryPath = fetchActionDirectoryPath;
|
|
1439
|
-
const emitFetchOutcome = async (event) => {
|
|
1440
|
-
await emitActionEventForCurrentSession({
|
|
1441
|
-
event,
|
|
1442
|
-
directoryPath: fetchActionDirectoryPath,
|
|
1443
|
-
});
|
|
1444
|
-
};
|
|
1445
|
-
let publishedSkillsResult = await loadPublishedSkillCatalog({
|
|
1446
|
-
directory: requestedDirectory,
|
|
1447
|
-
useCache: !args.refresh,
|
|
1448
|
-
signal: context.abort,
|
|
1583
|
+
publishedSkillsResult = await loadPublishedSkillCatalog({
|
|
1584
|
+
directory: requestedDirectory,
|
|
1585
|
+
useCache: false,
|
|
1586
|
+
signal: context.abort
|
|
1449
1587
|
});
|
|
1450
1588
|
if (publishedSkillsResult.fetchResult.ok) {
|
|
1451
|
-
|
|
1589
|
+
await scheduleInteractivePresenceStart();
|
|
1452
1590
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1591
|
+
} catch {
|
|
1592
|
+
// Return the original fetch failure with the latest login bootstrap snapshot attached.
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
if (!publishedSkillsResult.fetchResult.ok) {
|
|
1596
|
+
await emitFetchOutcome('FETCH_FAILED');
|
|
1597
|
+
return toFetchFailureOutput({
|
|
1598
|
+
worktree: input.worktree,
|
|
1599
|
+
config,
|
|
1600
|
+
publishedSkillsResult,
|
|
1601
|
+
loginBootstrapSnapshot: loginBootstrap.snapshot
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
const selection = selectPublishedSkills(publishedSkillsResult.fetchResult.payload, requestedSkills);
|
|
1605
|
+
const isSingleRequest = requestedSkills.length === 1;
|
|
1606
|
+
if (requestedSkills.length === 0) {
|
|
1607
|
+
const catalog = toPublishedSkillCatalog(publishedSkillsResult.fetchResult.payload);
|
|
1608
|
+
context.metadata({
|
|
1609
|
+
title: `opencode-wizard published skills catalog: ${catalog.publishedSkillCount}`,
|
|
1610
|
+
metadata: {
|
|
1611
|
+
...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
|
|
1612
|
+
status: 'ready',
|
|
1613
|
+
publishedSkillCount: catalog.publishedSkillCount.toString(),
|
|
1614
|
+
globalAssignmentCount: catalog.assignmentCounts.global.toString(),
|
|
1615
|
+
projectAssignmentCount: catalog.assignmentCounts.project.toString()
|
|
1470
1616
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1617
|
+
});
|
|
1618
|
+
await emitFetchOutcome('FETCH_SUCCESS');
|
|
1619
|
+
return {
|
|
1620
|
+
output: JSON.stringify({
|
|
1621
|
+
...catalog,
|
|
1622
|
+
status: 'ready',
|
|
1623
|
+
requestedDirectoryPath: publishedSkillsResult.directoryPath,
|
|
1624
|
+
workspaceResolution: toWorkspaceResolutionOutput(publishedSkillsResult.workspaceResolution),
|
|
1625
|
+
fetchedAt: publishedSkillsResult.fetchResult.fetchedAt,
|
|
1626
|
+
source: publishedSkillsResult.fetchResult.source,
|
|
1627
|
+
message: 'Catalog discovery only. Provide `skill` or `skills` to fetch markdown bodies/details for selected skills.'
|
|
1628
|
+
}, null, 2),
|
|
1629
|
+
metadata: {
|
|
1630
|
+
status: 'ready',
|
|
1631
|
+
...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
|
|
1632
|
+
publishedSkillCount: catalog.publishedSkillCount.toString()
|
|
1479
1633
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
},
|
|
1498
|
-
};
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
if (selection.selectedItems.length === 0 && isSingleRequest) {
|
|
1637
|
+
await emitFetchOutcome('FETCH_FAILED');
|
|
1638
|
+
return {
|
|
1639
|
+
output: JSON.stringify({
|
|
1640
|
+
pluginId: PLUGIN_ID,
|
|
1641
|
+
runtimeMode: 'tool_fetch_only',
|
|
1642
|
+
status: 'not_found',
|
|
1643
|
+
requestedDirectoryPath: publishedSkillsResult.directoryPath,
|
|
1644
|
+
workspaceResolution: toWorkspaceResolutionOutput(publishedSkillsResult.workspaceResolution),
|
|
1645
|
+
requestedSkill: requestedSkills[0],
|
|
1646
|
+
availableSkills: publishedSkillsResult.fetchResult.payload.skills.map(toPublishedSkillSummary)
|
|
1647
|
+
}, null, 2),
|
|
1648
|
+
metadata: {
|
|
1649
|
+
status: 'not_found',
|
|
1650
|
+
...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution)
|
|
1499
1651
|
}
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
let skillDetailResults = await Promise.all(selection.selectedItems.map(item => loadPublishedSkillDetail({
|
|
1655
|
+
workspaceResolution: publishedSkillsResult.workspaceResolution,
|
|
1656
|
+
item,
|
|
1657
|
+
signal: context.abort,
|
|
1658
|
+
useCache: !args.refresh,
|
|
1659
|
+
purpose: 'TOOL_FETCH'
|
|
1660
|
+
})));
|
|
1661
|
+
if (skillDetailResults.some(result => !result.ok && result.status === 'missing_auth')) {
|
|
1662
|
+
try {
|
|
1663
|
+
await startLoginCompletion('fetch').then(async authState => {
|
|
1664
|
+
await schedulePresenceStart(authState);
|
|
1665
|
+
});
|
|
1666
|
+
skillDetailResults = await Promise.all(selection.selectedItems.map(item => loadPublishedSkillDetail({
|
|
1667
|
+
workspaceResolution: publishedSkillsResult.workspaceResolution,
|
|
1668
|
+
item,
|
|
1669
|
+
signal: context.abort,
|
|
1670
|
+
useCache: false,
|
|
1671
|
+
purpose: 'TOOL_FETCH'
|
|
1505
1672
|
})));
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1673
|
+
} catch {
|
|
1674
|
+
// Return the original detail failure after the login bootstrap attempt updates snapshot state.
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
const failedSkillDetail = skillDetailResults.find(result => !result.ok);
|
|
1678
|
+
if (failedSkillDetail && !failedSkillDetail.ok) {
|
|
1679
|
+
await emitFetchOutcome('FETCH_FAILED');
|
|
1680
|
+
return failedSkillDetail;
|
|
1681
|
+
}
|
|
1682
|
+
const skillDetails = skillDetailResults.map(result => {
|
|
1683
|
+
if (!result.ok) {
|
|
1684
|
+
throw new Error('Published skill detail result unexpectedly missing after success guard.');
|
|
1685
|
+
}
|
|
1686
|
+
return result.detail;
|
|
1687
|
+
});
|
|
1688
|
+
if (isSingleRequest && skillDetails[0]) {
|
|
1689
|
+
const detail = skillDetails[0];
|
|
1690
|
+
context.metadata({
|
|
1691
|
+
title: `opencode-wizard published skill: ${detail.artifactName || detail.skillName}`,
|
|
1692
|
+
metadata: {
|
|
1693
|
+
...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
|
|
1694
|
+
skillSlug: detail.skillSlug,
|
|
1695
|
+
version: detail.version
|
|
1526
1696
|
}
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
return {
|
|
1545
|
-
output: JSON.stringify({
|
|
1546
|
-
pluginId: PLUGIN_ID,
|
|
1547
|
-
runtimeMode: 'tool_fetch_only',
|
|
1548
|
-
requestedDirectoryPath: publishedSkillsResult.directoryPath,
|
|
1549
|
-
workspaceResolution: toWorkspaceResolutionOutput(publishedSkillsResult.workspaceResolution),
|
|
1550
|
-
workspace: publishedSkillsResult.fetchResult.payload.workspace,
|
|
1551
|
-
fetchedAt: publishedSkillsResult.fetchResult.fetchedAt,
|
|
1552
|
-
source: publishedSkillsResult.fetchResult.source,
|
|
1553
|
-
skill: detail,
|
|
1554
|
-
}, null, 2),
|
|
1555
|
-
metadata: {
|
|
1556
|
-
status: 'ready',
|
|
1557
|
-
...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
|
|
1558
|
-
skillSlug: detail.skillSlug,
|
|
1559
|
-
},
|
|
1560
|
-
};
|
|
1697
|
+
});
|
|
1698
|
+
await emitFetchOutcome('FETCH_SUCCESS');
|
|
1699
|
+
return {
|
|
1700
|
+
output: JSON.stringify({
|
|
1701
|
+
pluginId: PLUGIN_ID,
|
|
1702
|
+
runtimeMode: 'tool_fetch_only',
|
|
1703
|
+
requestedDirectoryPath: publishedSkillsResult.directoryPath,
|
|
1704
|
+
workspaceResolution: toWorkspaceResolutionOutput(publishedSkillsResult.workspaceResolution),
|
|
1705
|
+
workspace: publishedSkillsResult.fetchResult.payload.workspace,
|
|
1706
|
+
fetchedAt: publishedSkillsResult.fetchResult.fetchedAt,
|
|
1707
|
+
source: publishedSkillsResult.fetchResult.source,
|
|
1708
|
+
skill: detail
|
|
1709
|
+
}, null, 2),
|
|
1710
|
+
metadata: {
|
|
1711
|
+
status: 'ready',
|
|
1712
|
+
...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
|
|
1713
|
+
skillSlug: detail.skillSlug
|
|
1561
1714
|
}
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
pluginId: PLUGIN_ID,
|
|
1574
|
-
runtimeMode: 'tool_fetch_only',
|
|
1575
|
-
requestedDirectoryPath: publishedSkillsResult.directoryPath,
|
|
1576
|
-
workspaceResolution: toWorkspaceResolutionOutput(publishedSkillsResult.workspaceResolution),
|
|
1577
|
-
workspace: publishedSkillsResult.fetchResult.payload.workspace,
|
|
1578
|
-
fetchedAt: publishedSkillsResult.fetchResult.fetchedAt,
|
|
1579
|
-
source: publishedSkillsResult.fetchResult.source,
|
|
1580
|
-
requestedSkills,
|
|
1581
|
-
missingSkills: selection.missingIdentifiers,
|
|
1582
|
-
skills: skillDetails,
|
|
1583
|
-
}, null, 2),
|
|
1584
|
-
metadata: {
|
|
1585
|
-
status: selection.missingIdentifiers.length > 0 ? 'partial' : 'ready',
|
|
1586
|
-
...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
|
|
1587
|
-
matchedCount: skillDetails.length.toString(),
|
|
1588
|
-
},
|
|
1589
|
-
};
|
|
1590
|
-
};
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
context.metadata({
|
|
1718
|
+
title: `opencode-wizard published skills fetch: ${skillDetails.length}`,
|
|
1719
|
+
metadata: {
|
|
1720
|
+
...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
|
|
1721
|
+
requestedCount: requestedSkills.length.toString(),
|
|
1722
|
+
matchedCount: skillDetails.length.toString()
|
|
1723
|
+
}
|
|
1724
|
+
});
|
|
1725
|
+
await emitFetchOutcome('FETCH_SUCCESS');
|
|
1591
1726
|
return {
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
},
|
|
1610
|
-
}),
|
|
1611
|
-
},
|
|
1612
|
-
'experimental.chat.system.transform': async (_hookInput, output) => {
|
|
1613
|
-
const publishedSkillsResult = await loadPublishedSkillCatalog({
|
|
1614
|
-
directory: input.directory,
|
|
1615
|
-
useCache: true,
|
|
1616
|
-
signal: AbortSignal.timeout(5_000),
|
|
1617
|
-
});
|
|
1618
|
-
const systemNote = buildSystemNote(publishedSkillsResult, config);
|
|
1619
|
-
if (!systemNote)
|
|
1620
|
-
return;
|
|
1621
|
-
output.system.push(systemNote);
|
|
1622
|
-
},
|
|
1727
|
+
output: JSON.stringify({
|
|
1728
|
+
pluginId: PLUGIN_ID,
|
|
1729
|
+
runtimeMode: 'tool_fetch_only',
|
|
1730
|
+
requestedDirectoryPath: publishedSkillsResult.directoryPath,
|
|
1731
|
+
workspaceResolution: toWorkspaceResolutionOutput(publishedSkillsResult.workspaceResolution),
|
|
1732
|
+
workspace: publishedSkillsResult.fetchResult.payload.workspace,
|
|
1733
|
+
fetchedAt: publishedSkillsResult.fetchResult.fetchedAt,
|
|
1734
|
+
source: publishedSkillsResult.fetchResult.source,
|
|
1735
|
+
requestedSkills,
|
|
1736
|
+
missingSkills: selection.missingIdentifiers,
|
|
1737
|
+
skills: skillDetails
|
|
1738
|
+
}, null, 2),
|
|
1739
|
+
metadata: {
|
|
1740
|
+
status: selection.missingIdentifiers.length > 0 ? 'partial' : 'ready',
|
|
1741
|
+
...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
|
|
1742
|
+
matchedCount: skillDetails.length.toString()
|
|
1743
|
+
}
|
|
1623
1744
|
};
|
|
1745
|
+
};
|
|
1746
|
+
return {
|
|
1747
|
+
tool: {
|
|
1748
|
+
opencode_wizard_published_skills_fetch: tool({
|
|
1749
|
+
description: 'Fetch one or multiple published skill bodies/details for the current scope; call with no args to discover the catalog and bootstrap auth when needed',
|
|
1750
|
+
args: {
|
|
1751
|
+
skill: tool.schema.string().optional().describe('Single skill slug, artifact name, or skill name'),
|
|
1752
|
+
skills: tool.schema.string().optional().describe('One or more comma-separated or newline-separated skill slugs, artifact names, or skill names'),
|
|
1753
|
+
directory: tool.schema.string().optional().describe('Optional absolute or relative directory override'),
|
|
1754
|
+
refresh: tool.schema.boolean().optional().describe('Bypass the local plugin cache for this request')
|
|
1755
|
+
},
|
|
1756
|
+
async execute(args, context) {
|
|
1757
|
+
return executePublishedSkillsFetchTool({
|
|
1758
|
+
args,
|
|
1759
|
+
context
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
})
|
|
1763
|
+
},
|
|
1764
|
+
'experimental.chat.system.transform': async (_hookInput, output) => {
|
|
1765
|
+
const publishedSkillsResult = await loadPublishedSkillCatalog({
|
|
1766
|
+
directory: input.directory,
|
|
1767
|
+
useCache: true,
|
|
1768
|
+
signal: AbortSignal.timeout(5_000)
|
|
1769
|
+
});
|
|
1770
|
+
if (!publishedSkillsResult.fetchResult.ok && publishedSkillsResult.fetchResult.status === 'missing_auth') {
|
|
1771
|
+
output.system.push('opencode-wizard plugin stored auth is missing, expired, or rejected. Passive startup will not open browser login; use opencode_wizard_published_skills_fetch interactively to authenticate when published skills are needed. No tokens are exposed.');
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
const details = await loadSystemNoteDetails({
|
|
1775
|
+
publishedSkillsResult,
|
|
1776
|
+
signal: AbortSignal.timeout(5_000)
|
|
1777
|
+
});
|
|
1778
|
+
const systemNote = buildSystemNote(publishedSkillsResult, config, details);
|
|
1779
|
+
if (!systemNote) return;
|
|
1780
|
+
output.system.push(systemNote);
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1624
1783
|
};
|
|
1625
1784
|
export default {
|
|
1626
|
-
|
|
1627
|
-
|
|
1785
|
+
id: PLUGIN_ID,
|
|
1786
|
+
server: OpencodeWizardSkillsPlugin
|
|
1628
1787
|
};
|
|
1788
|
+
//# sourceMappingURL=server.js.map
|