crewly 1.20.131 → 1.20.133
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/config/constants.ts +49 -0
- package/config/index.ts +2 -0
- package/config/skills/agent/core/find-skill/SKILL.md +58 -0
- package/config/skills/agent/core/find-skill/execute.sh +67 -0
- package/config/skills/agent/core/find-skill/execute.test.sh +69 -0
- package/config/skills/agent/core/generate-pdf/SKILL.md +24 -29
- package/config/skills/agent/core/generate-pdf/execute.sh +22 -78
- package/config/skills/agent/core/generate-pdf/execute.test.sh +45 -0
- package/config/skills/agent/core/install-skill/SKILL.md +67 -0
- package/config/skills/agent/core/install-skill/execute.sh +80 -0
- package/config/skills/agent/core/install-skill/execute.test.sh +74 -0
- package/config/skills/agent/pdf-tools/SKILL.md +103 -0
- package/config/skills/agent/pdf-tools/execute.sh +301 -0
- package/config/skills/agent/pdf-tools/execute.test.sh +218 -0
- package/config/skills/agent/pdf-tools/instructions.md +14 -0
- package/config/skills/agent/pdf-tools/pdf_tools.py +268 -0
- package/config/skills/agent/pdf-tools/skill.json +103 -0
- package/config/skills/agent/pdf-tools/style.css +143 -0
- package/config/skills/agent/transcribe-audio/SKILL.md +42 -5
- package/config/skills/agent/transcribe-audio/execute.sh +44 -11
- package/config/skills/agent/transcribe-audio/execute.test.sh +90 -0
- package/config/skills/agent/transcribe-audio/install-whisper-cpp.sh +126 -0
- package/config/skills/agent/transcribe-audio/install-whisper-cpp.test.sh +69 -0
- package/config/skills/agent/transcribe-audio/skill.json +67 -6
- package/config/skills/marketplace-registry.ts +8 -1
- package/config/skills/registry.json +337 -3
- package/dist/backend/backend/src/constants.d.ts +71 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +96 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.d.ts +56 -0
- package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.js +114 -0
- package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts +25 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +73 -55
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts +3 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.js +9 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.d.ts +49 -0
- package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.js +185 -0
- package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.d.ts +21 -0
- package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.js +29 -0
- package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.js.map +1 -0
- package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.js +5 -0
- package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts +14 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +33 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.d.ts +132 -0
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.d.ts.map +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.js +228 -0
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.js.map +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.d.ts +115 -0
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.js +160 -0
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.js.map +1 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +8 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -1
- package/dist/backend/backend/src/services/marketplace/marketplace.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/marketplace/marketplace.service.js +11 -3
- package/dist/backend/backend/src/services/marketplace/marketplace.service.js.map +1 -1
- package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.d.ts +281 -0
- package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.js +559 -0
- package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.js.map +1 -0
- package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.d.ts +243 -0
- package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.js +439 -0
- package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.js.map +1 -0
- package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.d.ts +138 -0
- package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.d.ts.map +1 -0
- package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.js +281 -0
- package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.js.map +1 -0
- package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.d.ts +338 -0
- package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.js +761 -0
- package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.js.map +1 -0
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +6 -0
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
- package/dist/backend/backend/src/types/marketplace.types.d.ts +16 -1
- package/dist/backend/backend/src/types/marketplace.types.d.ts.map +1 -1
- package/dist/backend/backend/src/utils/inbound-file-hint.utils.d.ts +55 -0
- package/dist/backend/backend/src/utils/inbound-file-hint.utils.d.ts.map +1 -0
- package/dist/backend/backend/src/utils/inbound-file-hint.utils.js +78 -0
- package/dist/backend/backend/src/utils/inbound-file-hint.utils.js.map +1 -0
- package/dist/backend/build-info.json +2 -2
- package/dist/backend/config/constants.d.ts +48 -0
- package/dist/backend/config/constants.d.ts.map +1 -1
- package/dist/backend/config/constants.js +48 -0
- package/dist/backend/config/constants.js.map +1 -1
- package/dist/backend/config/index.d.ts +2 -2
- package/dist/backend/config/index.d.ts.map +1 -1
- package/dist/backend/config/index.js +2 -2
- package/dist/backend/config/index.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +71 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +96 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts +14 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +33 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
- package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.d.ts +132 -0
- package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.d.ts.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.js +228 -0
- package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.js.map +1 -0
- package/dist/cli/backend/src/services/marketplace/marketplace.service.d.ts +101 -0
- package/dist/cli/backend/src/services/marketplace/marketplace.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/marketplace/marketplace.service.js +392 -0
- package/dist/cli/backend/src/services/marketplace/marketplace.service.js.map +1 -0
- package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.d.ts +281 -0
- package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.js +559 -0
- package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.js.map +1 -0
- package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.d.ts +138 -0
- package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.d.ts.map +1 -0
- package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.js +281 -0
- package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.js.map +1 -0
- package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.d.ts +338 -0
- package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.js +761 -0
- package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.js.map +1 -0
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +6 -0
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
- package/dist/cli/backend/src/types/marketplace.types.d.ts +219 -0
- package/dist/cli/backend/src/types/marketplace.types.d.ts.map +1 -0
- package/dist/cli/backend/src/types/marketplace.types.js +6 -0
- package/dist/cli/backend/src/types/marketplace.types.js.map +1 -0
- package/dist/cli/backend/src/utils/inbound-file-hint.utils.d.ts +55 -0
- package/dist/cli/backend/src/utils/inbound-file-hint.utils.d.ts.map +1 -0
- package/dist/cli/backend/src/utils/inbound-file-hint.utils.js +78 -0
- package/dist/cli/backend/src/utils/inbound-file-hint.utils.js.map +1 -0
- package/dist/cli/backend/src/utils/package-root.d.ts +15 -0
- package/dist/cli/backend/src/utils/package-root.d.ts.map +1 -0
- package/dist/cli/backend/src/utils/package-root.js +38 -0
- package/dist/cli/backend/src/utils/package-root.js.map +1 -0
- package/dist/cli/cli/src/commands/cloud.d.ts +45 -25
- package/dist/cli/cli/src/commands/cloud.d.ts.map +1 -1
- package/dist/cli/cli/src/commands/cloud.js +153 -47
- package/dist/cli/cli/src/commands/cloud.js.map +1 -1
- package/dist/cli/cli/src/commands/install.d.ts +13 -1
- package/dist/cli/cli/src/commands/install.d.ts.map +1 -1
- package/dist/cli/cli/src/commands/install.js +40 -3
- package/dist/cli/cli/src/commands/install.js.map +1 -1
- package/dist/cli/cli/src/commands/onboard-checklist.js +3 -3
- package/dist/cli/cli/src/commands/onboard-checklist.js.map +1 -1
- package/dist/cli/cli/src/commands/skills.d.ts +58 -0
- package/dist/cli/cli/src/commands/skills.d.ts.map +1 -0
- package/dist/cli/cli/src/commands/skills.js +132 -0
- package/dist/cli/cli/src/commands/skills.js.map +1 -0
- package/dist/cli/cli/src/index.js +18 -7
- package/dist/cli/cli/src/index.js.map +1 -1
- package/dist/cli/cli/src/utils/archive-creator.d.ts +14 -0
- package/dist/cli/cli/src/utils/archive-creator.d.ts.map +1 -1
- package/dist/cli/cli/src/utils/archive-creator.js +24 -3
- package/dist/cli/cli/src/utils/archive-creator.js.map +1 -1
- package/dist/cli/cli/src/utils/cloud-submit.d.ts.map +1 -1
- package/dist/cli/cli/src/utils/cloud-submit.js +14 -5
- package/dist/cli/cli/src/utils/cloud-submit.js.map +1 -1
- package/dist/cli/cli/src/utils/package-validator.d.ts +2 -0
- package/dist/cli/cli/src/utils/package-validator.d.ts.map +1 -1
- package/dist/cli/cli/src/utils/package-validator.js +16 -0
- package/dist/cli/cli/src/utils/package-validator.js.map +1 -1
- package/dist/cli/config/constants.d.ts +48 -0
- package/dist/cli/config/constants.d.ts.map +1 -1
- package/dist/cli/config/constants.js +48 -0
- package/dist/cli/config/constants.js.map +1 -1
- package/dist/cli/config/index.d.ts +2 -2
- package/dist/cli/config/index.d.ts.map +1 -1
- package/dist/cli/config/index.js +2 -2
- package/dist/cli/config/index.js.map +1 -1
- package/frontend/dist/assets/{index-5b98bf77.js → index-e6c403b6.js} +265 -265
- package/frontend/dist/index.html +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill install jobs — `install-skill` as a background job that reports back.
|
|
3
|
+
*
|
|
4
|
+
* An agent that lacks a capability runs `install-skill --id <skill>`. This
|
|
5
|
+
* service decides whether it may (trust rule), downloads the skill from the
|
|
6
|
+
* marketplace when it is not bundled (reusing the marketplace installer),
|
|
7
|
+
* runs its setup block, and — when the job ends — sends the requesting agent
|
|
8
|
+
* a message through the normal message queue (`system_event` to its
|
|
9
|
+
* session, the path event-bus notifications use). The agent is woken by that
|
|
10
|
+
* message and carries on: tells the user it is ready, then does the task it
|
|
11
|
+
* paused.
|
|
12
|
+
*
|
|
13
|
+
* ## Trust rule
|
|
14
|
+
*
|
|
15
|
+
* - **Official** skills (see skill-discovery.service) install without asking,
|
|
16
|
+
* including the system dependencies their setup block declares.
|
|
17
|
+
* - **Third-party** skills are refused unless the owner approved:
|
|
18
|
+
* - the owner acting from the dashboard (`X-Crewly-Caller: dashboard`, no
|
|
19
|
+
* agent header), or
|
|
20
|
+
* - an agent passing `approvedByOwner`, which is **verified**: there must be
|
|
21
|
+
* a genuine owner-authored chat message in the last
|
|
22
|
+
* OWNER_APPROVAL_LOOKBACK_MS that approves it — the same evidence the
|
|
23
|
+
* commitment-approval gate uses (chat-v2 `user` rows an agent cannot
|
|
24
|
+
* write). A claim with no such message is refused. If the chat cannot be
|
|
25
|
+
* read, the install is refused (fail closed: this runs someone else's code).
|
|
26
|
+
*
|
|
27
|
+
* @module services/skill-setup/skill-install-job.service
|
|
28
|
+
*/
|
|
29
|
+
import type { EnqueueMessageInput } from '../../types/messaging.types.js';
|
|
30
|
+
import type { MarketplaceItem, MarketplaceOperationResult } from '../../types/marketplace.types.js';
|
|
31
|
+
import { type SkillDiscoveryService } from './skill-discovery.service.js';
|
|
32
|
+
import { type SetupResult, type SkillSetupRunner } from './skill-setup-runner.service.js';
|
|
33
|
+
/** Job lifecycle. */
|
|
34
|
+
export type InstallJobState = 'running' | 'succeeded' | 'failed';
|
|
35
|
+
/** Public view of an install job. */
|
|
36
|
+
export interface SkillInstallJob {
|
|
37
|
+
jobId: string;
|
|
38
|
+
skillId: string;
|
|
39
|
+
state: InstallJobState;
|
|
40
|
+
official: boolean;
|
|
41
|
+
officialReason: string;
|
|
42
|
+
estimatedMinutes: number;
|
|
43
|
+
startedAt: string;
|
|
44
|
+
finishedAt?: string;
|
|
45
|
+
/** Sessions that get the completion message */
|
|
46
|
+
requesterSessions: string[];
|
|
47
|
+
/** What the requester was doing (echoed back so it can resume) */
|
|
48
|
+
resumeNote?: string;
|
|
49
|
+
/** Tail of the job log */
|
|
50
|
+
log: string;
|
|
51
|
+
/** One-line outcome */
|
|
52
|
+
message?: string;
|
|
53
|
+
/** Setup log file */
|
|
54
|
+
logFile?: string;
|
|
55
|
+
/** Completion messages enqueued */
|
|
56
|
+
notified: boolean;
|
|
57
|
+
/** Where the skill lives once installed */
|
|
58
|
+
executePath?: string;
|
|
59
|
+
}
|
|
60
|
+
/** Result of asking for an install. */
|
|
61
|
+
export type StartInstallResult = {
|
|
62
|
+
kind: 'job';
|
|
63
|
+
job: SkillInstallJob;
|
|
64
|
+
} | {
|
|
65
|
+
kind: 'already-ready';
|
|
66
|
+
skill: {
|
|
67
|
+
id: string;
|
|
68
|
+
executePath?: string;
|
|
69
|
+
officialReason: string;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
/** Input to {@link SkillInstallJobService.startInstall}. */
|
|
73
|
+
export interface StartInstallInput {
|
|
74
|
+
/** Skill id (local id, directory name or registry id) */
|
|
75
|
+
skillId: string;
|
|
76
|
+
/** Agent session to notify on completion (from X-Agent-Session) */
|
|
77
|
+
requesterSession?: string;
|
|
78
|
+
/** The agent says the owner approved (verified against owner chat) */
|
|
79
|
+
approvedByOwner?: boolean;
|
|
80
|
+
/** The request comes from the owner in the dashboard */
|
|
81
|
+
ownerDashboard?: boolean;
|
|
82
|
+
/** What the agent was doing, echoed in the completion message */
|
|
83
|
+
resumeNote?: string;
|
|
84
|
+
/** Re-run setup even when a check says everything is there */
|
|
85
|
+
force?: boolean;
|
|
86
|
+
/** What the agent quotes as the owner's approval (X-Agent-Authorization), recorded in the job log */
|
|
87
|
+
ownerClaim?: string;
|
|
88
|
+
}
|
|
89
|
+
/** Error with a machine-readable code for the REST layer. */
|
|
90
|
+
export declare class SkillInstallError extends Error {
|
|
91
|
+
readonly code: 'not_found' | 'owner_approval_required' | 'owner_approval_not_found' | 'owner_approval_unverifiable' | 'invalid_setup' | 'job_not_found';
|
|
92
|
+
readonly details: Record<string, unknown>;
|
|
93
|
+
/**
|
|
94
|
+
* @param code - `not_found`, `owner_approval_required`, `owner_approval_not_found`,
|
|
95
|
+
* `owner_approval_unverifiable`, `invalid_setup`, `job_not_found`
|
|
96
|
+
* @param message - Human-readable message, written for the agent
|
|
97
|
+
* @param details - Extra fields for the response
|
|
98
|
+
*/
|
|
99
|
+
constructor(code: 'not_found' | 'owner_approval_required' | 'owner_approval_not_found' | 'owner_approval_unverifiable' | 'invalid_setup' | 'job_not_found', message: string, details?: Record<string, unknown>);
|
|
100
|
+
}
|
|
101
|
+
/** Injectable dependencies. */
|
|
102
|
+
export interface SkillInstallJobDeps {
|
|
103
|
+
discovery?: SkillDiscoveryService;
|
|
104
|
+
runner?: SkillSetupRunner;
|
|
105
|
+
installMarketplaceItem?: (item: MarketplaceItem) => Promise<MarketplaceOperationResult>;
|
|
106
|
+
installPathFor?: (item: MarketplaceItem) => string;
|
|
107
|
+
/** Owner-authored chat messages since a time (throws when chat is unreadable) */
|
|
108
|
+
recentOwnerMessages?: (sinceMs: number) => string[];
|
|
109
|
+
/** Enqueue a message for an agent (null queue = cannot notify) */
|
|
110
|
+
enqueue?: ((input: EnqueueMessageInput) => void) | null;
|
|
111
|
+
now?: () => number;
|
|
112
|
+
idFactory?: () => string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Whether one owner message approves installing a skill.
|
|
116
|
+
*
|
|
117
|
+
* Counts a clear approval ("go ahead", "批准", "同意" — the commitment
|
|
118
|
+
* gate's tokens) or a short yes that names the skill ("ok install
|
|
119
|
+
* shady-ocr", "可以,装 shady-ocr"). A question never counts.
|
|
120
|
+
*
|
|
121
|
+
* @param message - Owner message text
|
|
122
|
+
* @param skill - Skill id and name
|
|
123
|
+
* @returns True when it approves
|
|
124
|
+
*/
|
|
125
|
+
export declare function isOwnerInstallApproval(message: string, skill: {
|
|
126
|
+
id: string;
|
|
127
|
+
name?: string;
|
|
128
|
+
}): boolean;
|
|
129
|
+
/**
|
|
130
|
+
* The message the requesting agent receives when a job ends.
|
|
131
|
+
*
|
|
132
|
+
* @param job - Finished job
|
|
133
|
+
* @param setup - Setup result (absent when the job failed before setup)
|
|
134
|
+
* @param durationMs - Job duration
|
|
135
|
+
* @returns Message text
|
|
136
|
+
*/
|
|
137
|
+
export declare function formatCompletionMessage(job: SkillInstallJob, setup: SetupResult | null, durationMs: number): string;
|
|
138
|
+
/** Runs and tracks skill install jobs. */
|
|
139
|
+
export declare class SkillInstallJobService {
|
|
140
|
+
private readonly discovery;
|
|
141
|
+
private readonly runner;
|
|
142
|
+
private readonly installMarketplaceItem;
|
|
143
|
+
private readonly installPathFor;
|
|
144
|
+
private readonly recentOwnerMessages;
|
|
145
|
+
private readonly enqueue;
|
|
146
|
+
private readonly now;
|
|
147
|
+
private readonly idFactory;
|
|
148
|
+
private readonly jobs;
|
|
149
|
+
private readonly logger;
|
|
150
|
+
/**
|
|
151
|
+
* @param deps - Injectable dependencies (all optional)
|
|
152
|
+
*/
|
|
153
|
+
constructor(deps?: SkillInstallJobDeps);
|
|
154
|
+
/**
|
|
155
|
+
* Ask for a skill to be installed and set up.
|
|
156
|
+
*
|
|
157
|
+
* Returns at once: either the skill is already usable, or a job id. When
|
|
158
|
+
* a job for the same skill is running, its id is returned and the new
|
|
159
|
+
* requester is added to the completion message's recipients.
|
|
160
|
+
*
|
|
161
|
+
* @param input - Skill id, requester, approval flags
|
|
162
|
+
* @returns The job, or `already-ready`
|
|
163
|
+
* @throws SkillInstallError when the skill is unknown, untrusted, or its setup block is invalid
|
|
164
|
+
*/
|
|
165
|
+
startInstall(input: StartInstallInput): Promise<StartInstallResult>;
|
|
166
|
+
/**
|
|
167
|
+
* Look up a job.
|
|
168
|
+
*
|
|
169
|
+
* @param jobId - Job id
|
|
170
|
+
* @returns The job
|
|
171
|
+
* @throws SkillInstallError `job_not_found`
|
|
172
|
+
*/
|
|
173
|
+
getJob(jobId: string): SkillInstallJob;
|
|
174
|
+
/**
|
|
175
|
+
* Wait for a job to finish (tests, CLI).
|
|
176
|
+
*
|
|
177
|
+
* @param jobId - Job id
|
|
178
|
+
* @returns The finished job
|
|
179
|
+
* @throws SkillInstallError `job_not_found`
|
|
180
|
+
*/
|
|
181
|
+
waitForJob(jobId: string): Promise<SkillInstallJob>;
|
|
182
|
+
/**
|
|
183
|
+
* Apply the trust rule.
|
|
184
|
+
*
|
|
185
|
+
* @param skill - Resolved skill
|
|
186
|
+
* @param input - Request
|
|
187
|
+
* @returns The owner message that approved a third-party install (undefined for official / dashboard)
|
|
188
|
+
* @throws SkillInstallError when a third-party skill lacks owner approval
|
|
189
|
+
*/
|
|
190
|
+
private checkTrust;
|
|
191
|
+
/**
|
|
192
|
+
* Install (when needed), set up, then notify.
|
|
193
|
+
*
|
|
194
|
+
* @param record - Job record (mutated)
|
|
195
|
+
* @param skill - Resolved skill
|
|
196
|
+
*/
|
|
197
|
+
private execute;
|
|
198
|
+
/**
|
|
199
|
+
* Read the raw `setup` block of an installed skill.
|
|
200
|
+
*
|
|
201
|
+
* @param skillDir - Skill directory
|
|
202
|
+
* @returns The block, or undefined
|
|
203
|
+
*/
|
|
204
|
+
private readSetup;
|
|
205
|
+
/**
|
|
206
|
+
* Send the completion message to every requester.
|
|
207
|
+
*
|
|
208
|
+
* @param record - Finished job (mutated: notified)
|
|
209
|
+
* @param setup - Setup result
|
|
210
|
+
* @param durationMs - Duration
|
|
211
|
+
*/
|
|
212
|
+
private notify;
|
|
213
|
+
/**
|
|
214
|
+
* Append to a job log, keeping the tail.
|
|
215
|
+
*
|
|
216
|
+
* @param record - Job
|
|
217
|
+
* @param text - Text
|
|
218
|
+
*/
|
|
219
|
+
private append;
|
|
220
|
+
/** Forget finished jobs after the retention window. */
|
|
221
|
+
private pruneFinished;
|
|
222
|
+
/**
|
|
223
|
+
* Public view of a job.
|
|
224
|
+
*
|
|
225
|
+
* @param r - Record
|
|
226
|
+
* @returns Job
|
|
227
|
+
*/
|
|
228
|
+
private toPublic;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* One-line "I'm installing it" text the agent can relay to the user.
|
|
232
|
+
*
|
|
233
|
+
* @param job - Job
|
|
234
|
+
* @returns Message
|
|
235
|
+
*/
|
|
236
|
+
export declare function describeStartedJob(job: SkillInstallJob): string;
|
|
237
|
+
/**
|
|
238
|
+
* The process-wide job service.
|
|
239
|
+
*
|
|
240
|
+
* @returns The service
|
|
241
|
+
*/
|
|
242
|
+
export declare function getSkillInstallJobService(): SkillInstallJobService;
|
|
243
|
+
//# sourceMappingURL=skill-install-job.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-install-job.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/skill-setup/skill-install-job.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill install jobs — `install-skill` as a background job that reports back.
|
|
3
|
+
*
|
|
4
|
+
* An agent that lacks a capability runs `install-skill --id <skill>`. This
|
|
5
|
+
* service decides whether it may (trust rule), downloads the skill from the
|
|
6
|
+
* marketplace when it is not bundled (reusing the marketplace installer),
|
|
7
|
+
* runs its setup block, and — when the job ends — sends the requesting agent
|
|
8
|
+
* a message through the normal message queue (`system_event` to its
|
|
9
|
+
* session, the path event-bus notifications use). The agent is woken by that
|
|
10
|
+
* message and carries on: tells the user it is ready, then does the task it
|
|
11
|
+
* paused.
|
|
12
|
+
*
|
|
13
|
+
* ## Trust rule
|
|
14
|
+
*
|
|
15
|
+
* - **Official** skills (see skill-discovery.service) install without asking,
|
|
16
|
+
* including the system dependencies their setup block declares.
|
|
17
|
+
* - **Third-party** skills are refused unless the owner approved:
|
|
18
|
+
* - the owner acting from the dashboard (`X-Crewly-Caller: dashboard`, no
|
|
19
|
+
* agent header), or
|
|
20
|
+
* - an agent passing `approvedByOwner`, which is **verified**: there must be
|
|
21
|
+
* a genuine owner-authored chat message in the last
|
|
22
|
+
* OWNER_APPROVAL_LOOKBACK_MS that approves it — the same evidence the
|
|
23
|
+
* commitment-approval gate uses (chat-v2 `user` rows an agent cannot
|
|
24
|
+
* write). A claim with no such message is refused. If the chat cannot be
|
|
25
|
+
* read, the install is refused (fail closed: this runs someone else's code).
|
|
26
|
+
*
|
|
27
|
+
* @module services/skill-setup/skill-install-job.service
|
|
28
|
+
*/
|
|
29
|
+
import { randomUUID } from 'crypto';
|
|
30
|
+
import * as fs from 'fs';
|
|
31
|
+
import * as path from 'path';
|
|
32
|
+
import { SKILL_SETUP_CONSTANTS, MESSAGE_SOURCES } from '../../constants.js';
|
|
33
|
+
import { LoggerService } from '../core/logger.service.js';
|
|
34
|
+
import { getInstallPath } from '../marketplace/marketplace.service.js';
|
|
35
|
+
import { installItem } from '../marketplace/marketplace-installer.service.js';
|
|
36
|
+
import { getMessageQueueInstance } from '../messaging/index.js';
|
|
37
|
+
import { containsApprovalToken } from '../orchestrator/commitment-approval-guard.js';
|
|
38
|
+
import { getChatV2Service } from '../chat-v2/chat-v2.singleton.js';
|
|
39
|
+
import { validateSetupManifest } from './skill-setup-manifest.js';
|
|
40
|
+
import { getSkillDiscoveryService } from './skill-discovery.service.js';
|
|
41
|
+
import { getSkillSetupRunner } from './skill-setup-runner.service.js';
|
|
42
|
+
/** Error with a machine-readable code for the REST layer. */
|
|
43
|
+
export class SkillInstallError extends Error {
|
|
44
|
+
code;
|
|
45
|
+
details;
|
|
46
|
+
/**
|
|
47
|
+
* @param code - `not_found`, `owner_approval_required`, `owner_approval_not_found`,
|
|
48
|
+
* `owner_approval_unverifiable`, `invalid_setup`, `job_not_found`
|
|
49
|
+
* @param message - Human-readable message, written for the agent
|
|
50
|
+
* @param details - Extra fields for the response
|
|
51
|
+
*/
|
|
52
|
+
constructor(code, message, details = {}) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.code = code;
|
|
55
|
+
this.details = details;
|
|
56
|
+
this.name = 'SkillInstallError';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Short affirmatives that approve an install when the message also names the skill. */
|
|
60
|
+
const SHORT_YES = [/\byes\b/i, /\bok(ay)?\b/i, /\bsure\b/i, /\binstall\b/i, /好/, /可以/, /行/, /装/, /同意/];
|
|
61
|
+
/** A question is never approval. */
|
|
62
|
+
const QUESTION = /[??]|吗\s*$|要不要|是否/;
|
|
63
|
+
/**
|
|
64
|
+
* Whether one owner message approves installing a skill.
|
|
65
|
+
*
|
|
66
|
+
* Counts a clear approval ("go ahead", "批准", "同意" — the commitment
|
|
67
|
+
* gate's tokens) or a short yes that names the skill ("ok install
|
|
68
|
+
* shady-ocr", "可以,装 shady-ocr"). A question never counts.
|
|
69
|
+
*
|
|
70
|
+
* @param message - Owner message text
|
|
71
|
+
* @param skill - Skill id and name
|
|
72
|
+
* @returns True when it approves
|
|
73
|
+
*/
|
|
74
|
+
export function isOwnerInstallApproval(message, skill) {
|
|
75
|
+
if (!message || QUESTION.test(message))
|
|
76
|
+
return false;
|
|
77
|
+
if (containsApprovalToken(message))
|
|
78
|
+
return true;
|
|
79
|
+
const lower = message.toLowerCase();
|
|
80
|
+
const names = [skill.id, skill.name].filter((n) => !!n && n.length >= 3).map((n) => n.toLowerCase());
|
|
81
|
+
return names.some((n) => lower.includes(n)) && SHORT_YES.some((re) => re.test(message));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Human duration.
|
|
85
|
+
*
|
|
86
|
+
* @param ms - Milliseconds
|
|
87
|
+
* @returns e.g. "4m 10s"
|
|
88
|
+
*/
|
|
89
|
+
function formatDuration(ms) {
|
|
90
|
+
const s = Math.max(0, Math.round(ms / 1000));
|
|
91
|
+
return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The message the requesting agent receives when a job ends.
|
|
95
|
+
*
|
|
96
|
+
* @param job - Finished job
|
|
97
|
+
* @param setup - Setup result (absent when the job failed before setup)
|
|
98
|
+
* @param durationMs - Job duration
|
|
99
|
+
* @returns Message text
|
|
100
|
+
*/
|
|
101
|
+
export function formatCompletionMessage(job, setup, durationMs) {
|
|
102
|
+
const H = SKILL_SETUP_CONSTANTS.COMPLETION_HEADERS;
|
|
103
|
+
const steps = setup?.steps.length
|
|
104
|
+
? setup.steps.map((s) => `${s.id} ${s.status === 'satisfied' ? 'already there' : s.status}`).join(' · ')
|
|
105
|
+
: '';
|
|
106
|
+
const resume = job.resumeNote ? `\nYou paused: "${job.resumeNote}"` : '';
|
|
107
|
+
if (job.state === 'succeeded') {
|
|
108
|
+
return [
|
|
109
|
+
`${H.SUCCEEDED} ${job.skillId} is installed and ready (job ${job.jobId}, ${formatDuration(durationMs)}).`,
|
|
110
|
+
...(steps ? [`Setup: ${steps}`] : []),
|
|
111
|
+
...(job.executePath ? [`Run it: bash ${job.executePath} (see SKILL.md next to it)`] : []),
|
|
112
|
+
].join('\n') + `${resume}\nNext: tell the user in one line that it is ready, then do the task you paused — now, without waiting to be asked.`;
|
|
113
|
+
}
|
|
114
|
+
return [
|
|
115
|
+
`${H.FAILED} ${job.skillId} (job ${job.jobId}, ${formatDuration(durationMs)}): ${job.message ?? 'unknown error'}`,
|
|
116
|
+
...(steps ? [`Setup: ${steps}`] : []),
|
|
117
|
+
...(job.logFile ? [`Log: ${job.logFile}`] : []),
|
|
118
|
+
].join('\n') + `${resume}\nNext: tell the user plainly what is missing and the fix quoted above (e.g. the command the owner can run). Do not retry the same install until something has changed.`;
|
|
119
|
+
}
|
|
120
|
+
/** Runs and tracks skill install jobs. */
|
|
121
|
+
export class SkillInstallJobService {
|
|
122
|
+
discovery;
|
|
123
|
+
runner;
|
|
124
|
+
installMarketplaceItem;
|
|
125
|
+
installPathFor;
|
|
126
|
+
recentOwnerMessages;
|
|
127
|
+
enqueue;
|
|
128
|
+
now;
|
|
129
|
+
idFactory;
|
|
130
|
+
jobs = new Map();
|
|
131
|
+
logger = LoggerService.getInstance().createComponentLogger('SkillInstallJobs');
|
|
132
|
+
/**
|
|
133
|
+
* @param deps - Injectable dependencies (all optional)
|
|
134
|
+
*/
|
|
135
|
+
constructor(deps = {}) {
|
|
136
|
+
this.discovery = deps.discovery ?? getSkillDiscoveryService();
|
|
137
|
+
this.runner = deps.runner ?? getSkillSetupRunner();
|
|
138
|
+
this.installMarketplaceItem = deps.installMarketplaceItem ?? installItem;
|
|
139
|
+
this.installPathFor = deps.installPathFor ?? ((item) => getInstallPath(item.type, item.id));
|
|
140
|
+
this.recentOwnerMessages = deps.recentOwnerMessages ?? ((since) => getChatV2Service().getRecentOwnerMessageContents(since));
|
|
141
|
+
// Resolved per job: the queue is created after this service at startup.
|
|
142
|
+
this.enqueue =
|
|
143
|
+
deps.enqueue !== undefined
|
|
144
|
+
? () => deps.enqueue ?? null
|
|
145
|
+
: () => {
|
|
146
|
+
const queue = getMessageQueueInstance();
|
|
147
|
+
return queue ? (input) => void queue.enqueue(input) : null;
|
|
148
|
+
};
|
|
149
|
+
this.now = deps.now ?? Date.now;
|
|
150
|
+
this.idFactory = deps.idFactory ?? (() => randomUUID().slice(0, 8));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Ask for a skill to be installed and set up.
|
|
154
|
+
*
|
|
155
|
+
* Returns at once: either the skill is already usable, or a job id. When
|
|
156
|
+
* a job for the same skill is running, its id is returned and the new
|
|
157
|
+
* requester is added to the completion message's recipients.
|
|
158
|
+
*
|
|
159
|
+
* @param input - Skill id, requester, approval flags
|
|
160
|
+
* @returns The job, or `already-ready`
|
|
161
|
+
* @throws SkillInstallError when the skill is unknown, untrusted, or its setup block is invalid
|
|
162
|
+
*/
|
|
163
|
+
async startInstall(input) {
|
|
164
|
+
const skill = await this.discovery.resolve(input.skillId);
|
|
165
|
+
if (!skill) {
|
|
166
|
+
throw new SkillInstallError('not_found', `No skill "${input.skillId}" is bundled with Crewly or listed in the marketplace. Run find-skill --query "<what you need>" to search.`);
|
|
167
|
+
}
|
|
168
|
+
// A job already running for this skill passed the trust check: join it.
|
|
169
|
+
this.pruneFinished();
|
|
170
|
+
const running = [...this.jobs.values()].find((j) => j.skillId === skill.id && j.state === 'running');
|
|
171
|
+
if (running) {
|
|
172
|
+
if (input.requesterSession && !running.requesterSessions.includes(input.requesterSession)) {
|
|
173
|
+
running.requesterSessions.push(input.requesterSession);
|
|
174
|
+
}
|
|
175
|
+
return { kind: 'job', job: this.toPublic(running) };
|
|
176
|
+
}
|
|
177
|
+
// Nothing to install: say so instead of making the agent wait for a message.
|
|
178
|
+
if (!input.force && skill.installed && !skill.manifestError) {
|
|
179
|
+
await this.discovery.probe(skill);
|
|
180
|
+
if (skill.ready) {
|
|
181
|
+
return { kind: 'already-ready', skill: { id: skill.id, executePath: skill.executePath, officialReason: skill.officialReason } };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const evidence = this.checkTrust(skill, input);
|
|
185
|
+
if (skill.manifestError) {
|
|
186
|
+
throw new SkillInstallError('invalid_setup', `The setup block of ${skill.id} is invalid, so it was not run: ${skill.manifestError}`);
|
|
187
|
+
}
|
|
188
|
+
const record = {
|
|
189
|
+
jobId: this.idFactory(),
|
|
190
|
+
skillId: skill.id,
|
|
191
|
+
state: 'running',
|
|
192
|
+
official: skill.official,
|
|
193
|
+
officialReason: skill.officialReason,
|
|
194
|
+
estimatedMinutes: skill.setup.estimatedMinutes ?? SKILL_SETUP_CONSTANTS.DEFAULT_ESTIMATED_MINUTES,
|
|
195
|
+
startedAt: new Date(this.now()).toISOString(),
|
|
196
|
+
requesterSessions: input.requesterSession ? [input.requesterSession] : [],
|
|
197
|
+
...(input.resumeNote ? { resumeNote: input.resumeNote } : {}),
|
|
198
|
+
log: evidence
|
|
199
|
+
? `Third-party skill approved by the owner in chat: "${evidence.slice(0, 300)}"` +
|
|
200
|
+
(input.ownerClaim ? `\nThe agent cited: "${input.ownerClaim.slice(0, 300)}"` : '') + '\n'
|
|
201
|
+
: input.ownerDashboard && !skill.official
|
|
202
|
+
? 'Third-party skill installed by the owner from the dashboard.\n'
|
|
203
|
+
: '',
|
|
204
|
+
notified: false,
|
|
205
|
+
executePath: skill.executePath,
|
|
206
|
+
finishedAtMs: null,
|
|
207
|
+
done: null,
|
|
208
|
+
};
|
|
209
|
+
this.jobs.set(record.jobId, record);
|
|
210
|
+
this.logger.info('Skill install started', { jobId: record.jobId, skillId: skill.id, official: skill.official, requesters: record.requesterSessions });
|
|
211
|
+
record.done = this.execute(record, skill);
|
|
212
|
+
return { kind: 'job', job: this.toPublic(record) };
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Look up a job.
|
|
216
|
+
*
|
|
217
|
+
* @param jobId - Job id
|
|
218
|
+
* @returns The job
|
|
219
|
+
* @throws SkillInstallError `job_not_found`
|
|
220
|
+
*/
|
|
221
|
+
getJob(jobId) {
|
|
222
|
+
const record = this.jobs.get(jobId);
|
|
223
|
+
if (!record)
|
|
224
|
+
throw new SkillInstallError('job_not_found', `Install job not found: ${jobId}`);
|
|
225
|
+
return this.toPublic(record);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Wait for a job to finish (tests, CLI).
|
|
229
|
+
*
|
|
230
|
+
* @param jobId - Job id
|
|
231
|
+
* @returns The finished job
|
|
232
|
+
* @throws SkillInstallError `job_not_found`
|
|
233
|
+
*/
|
|
234
|
+
async waitForJob(jobId) {
|
|
235
|
+
const record = this.jobs.get(jobId);
|
|
236
|
+
if (!record)
|
|
237
|
+
throw new SkillInstallError('job_not_found', `Install job not found: ${jobId}`);
|
|
238
|
+
if (record.done)
|
|
239
|
+
await record.done;
|
|
240
|
+
return this.toPublic(record);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Apply the trust rule.
|
|
244
|
+
*
|
|
245
|
+
* @param skill - Resolved skill
|
|
246
|
+
* @param input - Request
|
|
247
|
+
* @returns The owner message that approved a third-party install (undefined for official / dashboard)
|
|
248
|
+
* @throws SkillInstallError when a third-party skill lacks owner approval
|
|
249
|
+
*/
|
|
250
|
+
checkTrust(skill, input) {
|
|
251
|
+
if (skill.official || input.ownerDashboard)
|
|
252
|
+
return undefined;
|
|
253
|
+
const who = `"${skill.id}" is a third-party skill (${skill.officialReason})`;
|
|
254
|
+
if (!input.approvedByOwner) {
|
|
255
|
+
throw new SkillInstallError('owner_approval_required', `${who}, so it is not installed automatically. Ask the owner in chat whether to install it (say what it is and who publishes it). ` +
|
|
256
|
+
`Only after they say yes, run: install-skill --id ${skill.id} --approved-by-owner`, { official: false, officialReason: skill.officialReason });
|
|
257
|
+
}
|
|
258
|
+
let messages;
|
|
259
|
+
try {
|
|
260
|
+
messages = this.recentOwnerMessages(this.now() - SKILL_SETUP_CONSTANTS.OWNER_APPROVAL_LOOKBACK_MS);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
throw new SkillInstallError('owner_approval_unverifiable', `${who}. --approved-by-owner could not be checked because the owner's chat history is unreadable (${error instanceof Error ? error.message : String(error)}); not installing.`);
|
|
264
|
+
}
|
|
265
|
+
const evidence = messages.find((m) => isOwnerInstallApproval(m, { id: skill.id, name: skill.name }));
|
|
266
|
+
if (!evidence) {
|
|
267
|
+
const hours = Math.round(SKILL_SETUP_CONSTANTS.OWNER_APPROVAL_LOOKBACK_MS / 3_600_000);
|
|
268
|
+
throw new SkillInstallError('owner_approval_not_found', `${who}. --approved-by-owner was passed, but no owner message in the last ${hours}h approves installing it. ` +
|
|
269
|
+
`Ask the owner in chat first (name the skill); run this again after they say yes.`);
|
|
270
|
+
}
|
|
271
|
+
this.logger.info('Third-party skill install approved by owner message', { skillId: skill.id, evidence: evidence.slice(0, 200), claim: input.ownerClaim });
|
|
272
|
+
return evidence;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Install (when needed), set up, then notify.
|
|
276
|
+
*
|
|
277
|
+
* @param record - Job record (mutated)
|
|
278
|
+
* @param skill - Resolved skill
|
|
279
|
+
*/
|
|
280
|
+
async execute(record, skill) {
|
|
281
|
+
const started = this.now();
|
|
282
|
+
let setup = null;
|
|
283
|
+
try {
|
|
284
|
+
let skillDir = skill.skillDir;
|
|
285
|
+
let manifest = skill.manifest;
|
|
286
|
+
if (!skill.installed || !skillDir) {
|
|
287
|
+
const item = skill.registryItem;
|
|
288
|
+
if (!item)
|
|
289
|
+
throw new Error('not installed and not in the registry');
|
|
290
|
+
this.append(record, `Downloading ${item.name} v${item.version} from the marketplace…\n`);
|
|
291
|
+
let installed = item;
|
|
292
|
+
let res = await this.installMarketplaceItem(item);
|
|
293
|
+
if (!res.success && item.fallback) {
|
|
294
|
+
// Premium archives can be missing from the CDN; the public copy of the same skill installs.
|
|
295
|
+
this.append(record, `${res.message}; trying the public registry copy…\n`);
|
|
296
|
+
installed = item.fallback;
|
|
297
|
+
res = await this.installMarketplaceItem(item.fallback);
|
|
298
|
+
}
|
|
299
|
+
if (!res.success)
|
|
300
|
+
throw new Error(`marketplace install failed: ${res.message}`);
|
|
301
|
+
this.append(record, `${res.message}\n`);
|
|
302
|
+
skillDir = this.installPathFor(installed);
|
|
303
|
+
record.executePath = path.join(skillDir, 'execute.sh');
|
|
304
|
+
// The downloaded skill.json is authoritative for setup.
|
|
305
|
+
const local = this.readSetup(skillDir);
|
|
306
|
+
if (local !== undefined) {
|
|
307
|
+
const v = validateSetupManifest(local);
|
|
308
|
+
if (!v.valid)
|
|
309
|
+
throw new Error(`the installed skill's setup block is invalid: ${v.errors.join('; ')}`);
|
|
310
|
+
manifest = v.manifest;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (manifest) {
|
|
314
|
+
setup = await this.runner.runSetup({
|
|
315
|
+
skillId: skill.id,
|
|
316
|
+
skillDir,
|
|
317
|
+
manifest,
|
|
318
|
+
onProgress: (e) => this.append(record, `[${e.stepId}] ${e.phase}: ${e.message}\n`),
|
|
319
|
+
});
|
|
320
|
+
record.logFile = setup.logFile;
|
|
321
|
+
if (!setup.success)
|
|
322
|
+
throw new Error(setup.error ?? 'setup failed');
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
this.append(record, 'No setup needed.\n');
|
|
326
|
+
}
|
|
327
|
+
record.state = 'succeeded';
|
|
328
|
+
record.message = `${skill.id} is installed and ready`;
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
record.state = 'failed';
|
|
332
|
+
record.message = error instanceof Error ? error.message : String(error);
|
|
333
|
+
this.append(record, `FAILED: ${record.message}\n`);
|
|
334
|
+
}
|
|
335
|
+
record.finishedAtMs = this.now();
|
|
336
|
+
record.finishedAt = new Date(record.finishedAtMs).toISOString();
|
|
337
|
+
this.logger.info('Skill install finished', { jobId: record.jobId, skillId: record.skillId, state: record.state, message: record.message });
|
|
338
|
+
this.notify(record, setup, record.finishedAtMs - started);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Read the raw `setup` block of an installed skill.
|
|
342
|
+
*
|
|
343
|
+
* @param skillDir - Skill directory
|
|
344
|
+
* @returns The block, or undefined
|
|
345
|
+
*/
|
|
346
|
+
readSetup(skillDir) {
|
|
347
|
+
try {
|
|
348
|
+
return JSON.parse(fs.readFileSync(path.join(skillDir, 'skill.json'), 'utf-8')).setup;
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Send the completion message to every requester.
|
|
356
|
+
*
|
|
357
|
+
* @param record - Finished job (mutated: notified)
|
|
358
|
+
* @param setup - Setup result
|
|
359
|
+
* @param durationMs - Duration
|
|
360
|
+
*/
|
|
361
|
+
notify(record, setup, durationMs) {
|
|
362
|
+
if (record.requesterSessions.length === 0)
|
|
363
|
+
return;
|
|
364
|
+
const enqueue = this.enqueue();
|
|
365
|
+
if (!enqueue) {
|
|
366
|
+
this.logger.warn('Message queue unavailable; install result not delivered', { jobId: record.jobId });
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const content = formatCompletionMessage(this.toPublic(record), setup, durationMs);
|
|
370
|
+
for (const session of record.requesterSessions) {
|
|
371
|
+
try {
|
|
372
|
+
enqueue({
|
|
373
|
+
content,
|
|
374
|
+
conversationId: SKILL_SETUP_CONSTANTS.COMPLETION_CONVERSATION_ID,
|
|
375
|
+
source: MESSAGE_SOURCES.SYSTEM_EVENT,
|
|
376
|
+
targetSession: session,
|
|
377
|
+
});
|
|
378
|
+
record.notified = true;
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
this.logger.warn('Failed to enqueue install result', { jobId: record.jobId, session, error: error instanceof Error ? error.message : String(error) });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Append to a job log, keeping the tail.
|
|
387
|
+
*
|
|
388
|
+
* @param record - Job
|
|
389
|
+
* @param text - Text
|
|
390
|
+
*/
|
|
391
|
+
append(record, text) {
|
|
392
|
+
const next = record.log + text;
|
|
393
|
+
const max = SKILL_SETUP_CONSTANTS.JOB_LOG_MAX_CHARS;
|
|
394
|
+
record.log = next.length > max ? next.slice(next.length - max) : next;
|
|
395
|
+
}
|
|
396
|
+
/** Forget finished jobs after the retention window. */
|
|
397
|
+
pruneFinished() {
|
|
398
|
+
const cutoff = this.now() - SKILL_SETUP_CONSTANTS.JOB_RETENTION_MS;
|
|
399
|
+
for (const [id, job] of this.jobs) {
|
|
400
|
+
if (job.finishedAtMs !== null && job.finishedAtMs < cutoff)
|
|
401
|
+
this.jobs.delete(id);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Public view of a job.
|
|
406
|
+
*
|
|
407
|
+
* @param r - Record
|
|
408
|
+
* @returns Job
|
|
409
|
+
*/
|
|
410
|
+
toPublic(r) {
|
|
411
|
+
const view = { ...r, requesterSessions: [...r.requesterSessions] };
|
|
412
|
+
delete view.finishedAtMs;
|
|
413
|
+
delete view.done;
|
|
414
|
+
return view;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* One-line "I'm installing it" text the agent can relay to the user.
|
|
419
|
+
*
|
|
420
|
+
* @param job - Job
|
|
421
|
+
* @returns Message
|
|
422
|
+
*/
|
|
423
|
+
export function describeStartedJob(job) {
|
|
424
|
+
return (`Installing ${job.skillId} in the background (about ${job.estimatedMinutes} min). ` +
|
|
425
|
+
`Tell the user now, in one line, that you are installing it and roughly how long it takes; ` +
|
|
426
|
+
`you will get a ${SKILL_SETUP_CONSTANTS.COMPLETION_HEADERS.SUCCEEDED} or ${SKILL_SETUP_CONSTANTS.COMPLETION_HEADERS.FAILED} message when it ends — continue then.`);
|
|
427
|
+
}
|
|
428
|
+
let jobServiceSingleton = null;
|
|
429
|
+
/**
|
|
430
|
+
* The process-wide job service.
|
|
431
|
+
*
|
|
432
|
+
* @returns The service
|
|
433
|
+
*/
|
|
434
|
+
export function getSkillInstallJobService() {
|
|
435
|
+
if (!jobServiceSingleton)
|
|
436
|
+
jobServiceSingleton = new SkillInstallJobService();
|
|
437
|
+
return jobServiceSingleton;
|
|
438
|
+
}
|
|
439
|
+
//# sourceMappingURL=skill-install-job.service.js.map
|