Skip to content

Commit 52fce3b

Browse files
committed
feat: add project learning recommendations
1 parent 5f25a48 commit 52fce3b

6 files changed

Lines changed: 160 additions & 0 deletions

File tree

src/__tests__/extension-features-e2e.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,4 +419,48 @@ describe('extension RPC features', () => {
419419
sdk.respondToMcpInvocation(fixture.params)
420420
)).rejects.toThrow(/Invalid RPC result for autohand\.mcp\.invokeResponse/);
421421
});
422+
423+
it('gets project learning recommendations through the spawned CLI', async () => {
424+
const fixture = {
425+
method: 'autohand.learn.recommend',
426+
params: { deep: true },
427+
result: {
428+
success: true,
429+
projectSummary: 'TypeScript SDK',
430+
audit: [{
431+
skill: 'old-testing',
432+
status: 'outdated' as const,
433+
reason: 'Uses a retired command',
434+
}],
435+
recommendations: [{
436+
slug: 'typescript-best-practices',
437+
score: 0.97,
438+
reason: 'Matches this repository',
439+
}],
440+
gapAnalysis: null,
441+
},
442+
};
443+
444+
await expect(withSDK(fixture, (sdk) =>
445+
sdk.getLearningRecommendations(fixture.params)
446+
)).resolves.toEqual(fixture.result);
447+
});
448+
449+
it('rejects malformed project learning recommendation scores', async () => {
450+
const fixture = {
451+
method: 'autohand.learn.recommend',
452+
params: {},
453+
result: {
454+
success: true,
455+
projectSummary: 'SDK',
456+
audit: [],
457+
recommendations: [{ slug: 'testing', score: 'high', reason: 'Useful' }],
458+
gapAnalysis: 'Needs integration testing',
459+
},
460+
};
461+
462+
await expect(withSDK(fixture, (sdk) =>
463+
sdk.getLearningRecommendations(fixture.params)
464+
)).rejects.toThrow(/Invalid RPC result for autohand\.learn\.recommend/);
465+
});
422466
});

src/rpc/client.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ import type {
6666
McpSetVscodeToolsResult,
6767
McpInvokeResponseParams,
6868
McpInvokeResponseResult,
69+
LearnRecommendParams,
70+
LearnRecommendResult,
6971
PermissionResponseParams,
7072
SDKEvent,
7173
JsonRpcParams,
@@ -580,6 +582,14 @@ export class RPCClient {
580582
return validateExtensionRpcResult('autohand.mcp.invokeResponse', result);
581583
}
582584

585+
/** Audit installed skills and recommend project-relevant additions. */
586+
async getLearningRecommendations(
587+
params: LearnRecommendParams = {}
588+
): Promise<LearnRecommendResult> {
589+
const result = await this.transport.request('autohand.learn.recommend', params);
590+
return validateExtensionRpcResult('autohand.learn.recommend', result);
591+
}
592+
583593
/**
584594
* Set permission mode
585595
*

src/sdk/agent.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import type {
2525
McpSetVscodeToolsResult,
2626
McpInvokeResponseParams,
2727
McpInvokeResponseResult,
28+
LearnRecommendParams,
29+
LearnRecommendResult,
2830
PermissionResponseParams,
2931
PromptParams,
3032
QueueGoalParams,
@@ -767,4 +769,10 @@ export class Agent {
767769
): Promise<McpInvokeResponseResult> {
768770
return this.sdk.respondToMcpInvocation(params);
769771
}
772+
773+
async getLearningRecommendations(
774+
params: LearnRecommendParams = {}
775+
): Promise<LearnRecommendResult> {
776+
return this.sdk.getLearningRecommendations(params);
777+
}
770778
}

src/sdk/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ import type {
6767
McpSetVscodeToolsResult,
6868
McpInvokeResponseParams,
6969
McpInvokeResponseResult,
70+
LearnRecommendParams,
71+
LearnRecommendResult,
7072
PermissionResponseParams,
7173
SDKEvent,
7274
ModelInfo,
@@ -1627,6 +1629,14 @@ export class AutohandSDK {
16271629
return this.client.respondToMcpInvocation(params);
16281630
}
16291631

1632+
/** Audit installed skills and recommend project-relevant additions. */
1633+
async getLearningRecommendations(
1634+
params: LearnRecommendParams = {}
1635+
): Promise<LearnRecommendResult> {
1636+
await this.ensureStarted();
1637+
return this.client.getLearningRecommendations(params);
1638+
}
1639+
16301640
/**
16311641
* Allow a pending permission request.
16321642
*

src/types/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2012,6 +2012,31 @@ export interface McpInvokeResponseResult {
20122012
success: boolean;
20132013
}
20142014

2015+
export interface LearnRecommendParams {
2016+
deep?: boolean;
2017+
}
2018+
2019+
export interface LearnAuditEntry {
2020+
skill: string;
2021+
status: 'redundant' | 'outdated' | 'conflicting';
2022+
reason: string;
2023+
}
2024+
2025+
export interface LearnRecommendation {
2026+
slug: string;
2027+
score: number;
2028+
reason: string;
2029+
}
2030+
2031+
export interface LearnRecommendResult {
2032+
success: boolean;
2033+
projectSummary: string;
2034+
audit: LearnAuditEntry[];
2035+
recommendations: LearnRecommendation[];
2036+
gapAnalysis: string | null;
2037+
error?: string;
2038+
}
2039+
20152040
// ============================================================================
20162041
// RPC Response Results
20172042
// ============================================================================

src/validation/extension-rpc-results.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import type {
99
YoloSetResult,
1010
McpSetVscodeToolsResult,
1111
McpInvokeResponseResult,
12+
LearnAuditEntry,
13+
LearnRecommendation,
14+
LearnRecommendResult,
1215
RpcHistoryEntry,
1316
DirectoryAccessResponseResult,
1417
DirectoryAccessAcknowledgedResult,
@@ -256,6 +259,63 @@ function mcpInvokeResponseResult(
256259
return { success: boolean(record.success, method, `${path}.success`) };
257260
}
258261

262+
function learnAuditStatus(
263+
value: unknown,
264+
method: string,
265+
path: string
266+
): LearnAuditEntry['status'] {
267+
if (value === 'redundant' || value === 'outdated' || value === 'conflicting') return value;
268+
return invalid(method, path, 'redundant | outdated | conflicting', value);
269+
}
270+
271+
function learnAuditEntry(value: unknown, method: string, path: string): LearnAuditEntry {
272+
const record = object(value, method, path);
273+
return {
274+
skill: string(record.skill, method, `${path}.skill`),
275+
status: learnAuditStatus(record.status, method, `${path}.status`),
276+
reason: string(record.reason, method, `${path}.reason`),
277+
};
278+
}
279+
280+
function learnRecommendation(
281+
value: unknown,
282+
method: string,
283+
path: string
284+
): LearnRecommendation {
285+
const record = object(value, method, path);
286+
return {
287+
slug: string(record.slug, method, `${path}.slug`),
288+
score: number(record.score, method, `${path}.score`),
289+
reason: string(record.reason, method, `${path}.reason`),
290+
};
291+
}
292+
293+
function learnRecommendResult(
294+
value: unknown,
295+
method: string,
296+
path: string
297+
): LearnRecommendResult {
298+
const record = object(value, method, path);
299+
const result: LearnRecommendResult = {
300+
success: boolean(record.success, method, `${path}.success`),
301+
projectSummary: string(record.projectSummary, method, `${path}.projectSummary`),
302+
audit: array(record.audit, method, `${path}.audit`, learnAuditEntry),
303+
recommendations: array(
304+
record.recommendations,
305+
method,
306+
`${path}.recommendations`,
307+
learnRecommendation
308+
),
309+
gapAnalysis: record.gapAnalysis === null
310+
? null
311+
: string(record.gapAnalysis, method, `${path}.gapAnalysis`),
312+
};
313+
if (record.error !== undefined) {
314+
result.error = string(record.error, method, `${path}.error`);
315+
}
316+
return result;
317+
}
318+
259319
interface ExtensionRpcResultMap {
260320
'autohand.permissionAcknowledged': PermissionAcknowledgedResult;
261321
'autohand.directoryAccessResponse': DirectoryAccessResponseResult;
@@ -268,6 +328,7 @@ interface ExtensionRpcResultMap {
268328
'autohand.yolo.set': YoloSetResult;
269329
'autohand.mcp.setVscodeTools': McpSetVscodeToolsResult;
270330
'autohand.mcp.invokeResponse': McpInvokeResponseResult;
331+
'autohand.learn.recommend': LearnRecommendResult;
271332
}
272333

273334
export type ExtensionRpcMethod = keyof ExtensionRpcResultMap;
@@ -297,6 +358,8 @@ const validators: {
297358
mcpSetVscodeToolsResult(value, 'autohand.mcp.setVscodeTools', path),
298359
'autohand.mcp.invokeResponse': (value, path) =>
299360
mcpInvokeResponseResult(value, 'autohand.mcp.invokeResponse', path),
361+
'autohand.learn.recommend': (value, path) =>
362+
learnRecommendResult(value, 'autohand.learn.recommend', path),
300363
};
301364

302365
export function validateExtensionRpcResult<Method extends ExtensionRpcMethod>(

0 commit comments

Comments
 (0)