mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-07 07:49:02 +00:00
feat(agent-core-v2): surface a machine-key note from capability installs
CapabilityEntry.install now resolves an optional note exposed through CapabilityInstallProgress.note (wire-visible). The webbridge entry returns 'user-skill-migrated' when it replaces a pre-existing user-source skill (from the official installer) with the plugin-managed copy — clients can localize the migration instead of the skill silently disappearing from the user's directory.
This commit is contained in:
parent
431c1f12ec
commit
1781895546
7 changed files with 58 additions and 15 deletions
|
|
@ -126,13 +126,16 @@ export class CapabilityService extends Disposable implements ICapabilityService
|
|||
// Fire-and-forget: progress and errors are surfaced through polling.
|
||||
void (async () => {
|
||||
try {
|
||||
await entry.install((step, percent) => {
|
||||
const note = await entry.install((step, percent) => {
|
||||
this.installProgress.set(
|
||||
entry.id,
|
||||
percent === undefined ? { running: true, step } : { running: true, step, percent },
|
||||
);
|
||||
});
|
||||
this.installProgress.set(entry.id, { running: false });
|
||||
this.installProgress.set(
|
||||
entry.id,
|
||||
note === undefined ? { running: false } : { running: false, note },
|
||||
);
|
||||
} catch (error) {
|
||||
this.installProgress.set(entry.id, {
|
||||
running: false,
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ export function createKimiCuEntry(ctx: CapabilityEntryContext): CapabilityEntry
|
|||
}
|
||||
}
|
||||
|
||||
async function install(report: CapabilityInstallReporter): Promise<void> {
|
||||
async function install(report: CapabilityInstallReporter): Promise<string | undefined> {
|
||||
if (!supported) {
|
||||
throw new Error(`kimi-cu is only supported on macOS (current: ${ctx.platform})`);
|
||||
}
|
||||
|
|
@ -240,6 +240,7 @@ export function createKimiCuEntry(ctx: CapabilityEntryContext): CapabilityEntry
|
|||
await runCommand(ctx.hostProcess, appBin, ['request-permissions', '--ax', '--screen'], {
|
||||
timeout: PERMISSIONS_TIMEOUT_MS,
|
||||
});
|
||||
return undefined;
|
||||
} finally {
|
||||
await rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ export function createKimiWebbridgeEntry(ctx: CapabilityEntryContext): Capabilit
|
|||
throw new Error(`WebBridge daemon did not come up on ${baseUrl} — check ~/.kimi-webbridge/logs`);
|
||||
}
|
||||
|
||||
async function install(report: CapabilityInstallReporter): Promise<void> {
|
||||
async function install(report: CapabilityInstallReporter): Promise<string | undefined> {
|
||||
const asset = binaryAssetName(ctx.platform, ctx.arch);
|
||||
if (asset === undefined) {
|
||||
throw new Error(`kimi-webbridge is not supported on ${ctx.platform}/${ctx.arch}`);
|
||||
|
|
@ -188,8 +188,12 @@ export function createKimiWebbridgeEntry(ctx: CapabilityEntryContext): Capabilit
|
|||
report('skill');
|
||||
await ctx.plugins.installPlugin({ source: PLUGIN_ZIP_URL });
|
||||
// Un-shadow the plugin copy: the user-source skill (priority 20) wins
|
||||
// over the plugin source (priority 5) on name collisions.
|
||||
// over the plugin source (priority 5) on name collisions. Surface the
|
||||
// migration so clients can tell the user their manually-installed skill
|
||||
// is now managed as a plugin.
|
||||
const hadUserSourceSkill = await exists(userSourceSkillDir);
|
||||
await rm(userSourceSkillDir, { recursive: true, force: true });
|
||||
return hadUserSourceSkill ? 'user-skill-migrated' : undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,13 @@ export interface CapabilityInstallProgress {
|
|||
readonly percent?: number;
|
||||
/** Set when the last install attempt failed; cleared on the next attempt. */
|
||||
readonly error?: string;
|
||||
/**
|
||||
* Machine-key note from the last completed install (e.g.
|
||||
* 'user-skill-migrated' — a pre-existing user-source skill was replaced by
|
||||
* the plugin-managed copy). Clients localize it; cleared on the next
|
||||
* attempt.
|
||||
*/
|
||||
readonly note?: string;
|
||||
}
|
||||
|
||||
export interface CapabilityDetectResult {
|
||||
|
|
@ -79,5 +86,9 @@ export interface CapabilityEntry {
|
|||
*/
|
||||
readonly wiringStepId: string;
|
||||
detect(): Promise<CapabilityDetectResult>;
|
||||
install(report: CapabilityInstallReporter): Promise<void>;
|
||||
/**
|
||||
* Resolves with an optional machine-key note surfaced through
|
||||
* `CapabilityInstallProgress.note` (e.g. 'user-skill-migrated').
|
||||
*/
|
||||
install(report: CapabilityInstallReporter): Promise<string | undefined>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ function fakeEntry(overrides: {
|
|||
supported?: boolean;
|
||||
wiringStepId?: string;
|
||||
detect?: CapabilityDetectResult;
|
||||
install?: (report: CapabilityInstallReporter) => Promise<void>;
|
||||
install?: (report: CapabilityInstallReporter) => Promise<string | undefined>;
|
||||
}): CapabilityEntry {
|
||||
return {
|
||||
id: overrides.id,
|
||||
|
|
@ -33,7 +33,7 @@ function fakeEntry(overrides: {
|
|||
Promise.resolve(
|
||||
overrides.detect ?? { steps: [{ id: 'plugin', state: 'ok' }] },
|
||||
),
|
||||
install: overrides.install ?? (() => Promise.resolve()),
|
||||
install: overrides.install ?? (() => Promise.resolve(undefined)),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -164,8 +164,8 @@ describe('CapabilityService', () => {
|
|||
id: 'kimi-cu',
|
||||
install: (report) => {
|
||||
report('download', 42);
|
||||
return new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
return new Promise<string | undefined>((resolve) => {
|
||||
release = () => resolve(undefined);
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
|
@ -199,6 +199,22 @@ describe('CapabilityService', () => {
|
|||
expect.unreachable('install never settled');
|
||||
});
|
||||
|
||||
it('surfaces an install note from the entry through progress', async () => {
|
||||
const service = fakeService([
|
||||
fakeEntry({
|
||||
id: 'kimi-cu',
|
||||
install: () => Promise.resolve('user-skill-migrated'),
|
||||
}),
|
||||
]);
|
||||
await service.installCapability('kimi-cu');
|
||||
for (let i = 0; i < 50; i += 1) {
|
||||
const status = await service.getCapability('kimi-cu');
|
||||
if (!status.install.running) break;
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
expect((await service.getCapability('kimi-cu')).install.note).toBe('user-skill-migrated');
|
||||
});
|
||||
|
||||
it('surfaces install errors through progress until the next attempt', async () => {
|
||||
let attempts = 0;
|
||||
const service = fakeService([
|
||||
|
|
@ -206,7 +222,9 @@ describe('CapabilityService', () => {
|
|||
id: 'kimi-cu',
|
||||
install: () => {
|
||||
attempts += 1;
|
||||
return attempts === 1 ? Promise.reject(new Error('boom')) : Promise.resolve();
|
||||
return attempts === 1
|
||||
? Promise.reject(new Error('boom'))
|
||||
: Promise.resolve(undefined);
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
|
@ -237,7 +255,7 @@ describe('CapabilityService shelf-install hook', () => {
|
|||
id: 'kimi-cu' | 'kimi-webbridge';
|
||||
wiringStepId?: string;
|
||||
steps: Array<{ id: string; state: 'ok' | 'missing'; optional?: boolean }>;
|
||||
install?: () => Promise<void>;
|
||||
install?: () => Promise<string | undefined>;
|
||||
}) {
|
||||
const state = { steps: opts.steps };
|
||||
let installs = 0;
|
||||
|
|
@ -247,7 +265,7 @@ describe('CapabilityService shelf-install hook', () => {
|
|||
detect: { steps: state.steps },
|
||||
install: () => {
|
||||
installs += 1;
|
||||
return (opts.install ?? (() => Promise.resolve()))();
|
||||
return (opts.install ?? (() => Promise.resolve(undefined)))();
|
||||
},
|
||||
});
|
||||
// Re-read the mutable step list on every detect.
|
||||
|
|
|
|||
|
|
@ -215,7 +215,10 @@ describe('kimi-webbridge entry', () => {
|
|||
makeCtx({ plugins: plugins.service, hostProcess: host.service, fetchImpl }),
|
||||
);
|
||||
|
||||
await entry.install((step, percent) => reports.push([step, percent]));
|
||||
const note = await entry.install((step, percent) => reports.push([step, percent]));
|
||||
|
||||
// Migration note surfaced (the pre-existing user skill was replaced).
|
||||
expect(note).toBe('user-skill-migrated');
|
||||
|
||||
// Binary downloaded into place and made executable.
|
||||
const binPath = path.join(root, 'user-home', '.kimi-webbridge', 'bin', 'kimi-webbridge');
|
||||
|
|
@ -244,8 +247,10 @@ describe('kimi-webbridge entry', () => {
|
|||
makeCtx({ plugins: plugins.service, hostProcess: host.service, fetchImpl }),
|
||||
);
|
||||
|
||||
await entry.install(() => {});
|
||||
const note = await entry.install(() => {});
|
||||
expect(host.calls).toEqual([]);
|
||||
// No pre-existing user skill → no migration note.
|
||||
expect(note).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects install on unsupported platforms before any side effect', async () => {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export const capabilityInstallProgressSchema = z.object({
|
|||
step: z.string().optional(),
|
||||
percent: z.number().min(0).max(100).optional(),
|
||||
error: z.string().optional(),
|
||||
note: z.string().optional(),
|
||||
});
|
||||
export type CapabilityInstallProgressWire = z.infer<typeof capabilityInstallProgressSchema>;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue