peaks-loop 4.0.7 → 4.0.8
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/CHANGELOG.md +46 -288
- package/dist/cli/commands/code-job-shape-commands.js +8 -11
- package/dist/cli/commands/code-mode-gate-commands.js +1 -43
- package/dist/cli/commands/code-runtime-commands.js +36 -14
- package/dist/cli/commands/core/skill-command.js +75 -13
- package/dist/cli/commands/core/standards-command.js +5 -50
- package/dist/cli/commands/dispatch-commands.d.ts +20 -0
- package/dist/cli/commands/dispatch-commands.js +39 -38
- package/dist/cli/commands/dispatch-from-dag.js +17 -0
- package/dist/cli/commands/heartbeat-commands.d.ts +21 -0
- package/dist/cli/commands/heartbeat-commands.js +40 -0
- package/dist/cli/commands/hook-handle.js +5 -2
- package/dist/cli/commands/hooks-commands.js +9 -26
- package/dist/cli/commands/loop-eval-commands.js +5 -2
- package/dist/cli/commands/sub-agent-commands.js +0 -2
- package/dist/cli/commands/sub-agent-shared.d.ts +7 -9
- package/dist/cli/commands/workflow-commands.js +8 -0
- package/dist/cli/commands/workflow-lifecycle-commands.d.ts +63 -0
- package/dist/cli/commands/workflow-lifecycle-commands.js +302 -0
- package/dist/cli/commands/workspace/init-command.js +19 -0
- package/dist/services/audit/backing-detector.d.ts +2 -19
- package/dist/services/audit/backing-detector.js +7 -66
- package/dist/services/audit/enforcers/active-skill-resolver.d.ts +25 -9
- package/dist/services/audit/enforcers/active-skill-resolver.js +83 -21
- package/dist/services/audit/enforcers/lint-audit-regression.js +1 -10
- package/dist/services/code/auto-compact-orchestrator.d.ts +35 -2
- package/dist/services/code/auto-compact-orchestrator.js +29 -9
- package/dist/services/context/build-dispatch-system-prompt.js +0 -18
- package/dist/services/dispatch/dispatch-record-writer.d.ts +25 -1
- package/dist/services/dispatch/dispatch-record-writer.js +85 -15
- package/dist/services/dispatch/test-tool-detection.d.ts +0 -14
- package/dist/services/dispatch/test-tool-detection.js +0 -22
- package/dist/services/doctor/doctor-service/checks/skill-presence.d.ts +8 -0
- package/dist/services/doctor/doctor-service/checks/skill-presence.js +9 -1
- package/dist/services/hooks/presence-marker-detector.d.ts +6 -0
- package/dist/services/hooks/presence-marker-detector.js +48 -0
- package/dist/services/ide/adapters/claude-code-adapter.js +20 -0
- package/dist/services/ide/adapters/codex-adapter.js +20 -1
- package/dist/services/ide/adapters/cursor-adapter.js +21 -1
- package/dist/services/ide/adapters/hermes-adapter.js +20 -1
- package/dist/services/ide/adapters/openclaw-adapter.js +20 -1
- package/dist/services/ide/adapters/qoder-adapter.js +20 -1
- package/dist/services/ide/adapters/tongyi-lingma-adapter.js +20 -1
- package/dist/services/ide/adapters/trae-adapter.js +21 -1
- package/dist/services/ide/adapters/zcode-adapter.js +19 -0
- package/dist/services/ide/ide-types.d.ts +15 -0
- package/dist/services/observability/observability-service.d.ts +2 -2
- package/dist/services/scan/archetype-service.js +2 -52
- package/dist/services/session/caller-binding-service.d.ts +21 -0
- package/dist/services/session/caller-binding-service.js +35 -0
- package/dist/services/session/caller-id-types.d.ts +62 -13
- package/dist/services/session/caller-id-types.js +25 -13
- package/dist/services/session/index.d.ts +4 -2
- package/dist/services/session/index.js +8 -2
- package/dist/services/session/platform-fallbacks.d.ts +25 -17
- package/dist/services/session/platform-fallbacks.js +26 -28
- package/dist/services/session/resolve-caller-id.d.ts +64 -29
- package/dist/services/session/resolve-caller-id.js +122 -48
- package/dist/services/session/session-manager.d.ts +0 -29
- package/dist/services/session/session-manager.js +0 -33
- package/dist/services/skills/hooks-settings-service.d.ts +0 -41
- package/dist/services/skills/hooks-settings-service.js +5 -61
- package/dist/services/skills/presence-lease-service.d.ts +73 -0
- package/dist/services/skills/presence-lease-service.js +358 -0
- package/dist/services/skills/presence-lease-types.d.ts +75 -0
- package/dist/services/skills/presence-lease-types.js +16 -0
- package/dist/services/skills/skill-presence-service.js +86 -22
- package/dist/services/slice/slice-check-service.js +0 -59
- package/dist/services/standards/project-standards-service.js +0 -44
- package/dist/services/workflow/workflow-graph-store.d.ts +77 -0
- package/dist/services/workflow/workflow-graph-store.js +278 -0
- package/dist/services/workflow/workflow-graph-types.d.ts +56 -0
- package/dist/services/workflow/workflow-graph-types.js +67 -0
- package/dist/services/workflow/workflow-inflight-probe.d.ts +52 -0
- package/dist/services/workflow/workflow-inflight-probe.js +86 -0
- package/dist/services/workflow/workflow-node-lifecycle.d.ts +67 -0
- package/dist/services/workflow/workflow-node-lifecycle.js +348 -0
- package/dist/services/workflow/workflow-presence-lifecycle.d.ts +58 -0
- package/dist/services/workflow/workflow-presence-lifecycle.js +196 -0
- package/dist/services/workspace/reconcile-service.d.ts +31 -0
- package/dist/services/workspace/reconcile-service.js +132 -1
- package/package.json +4 -4
- package/skills/peaks-code/SKILL.md +5 -43
- package/skills/peaks-code/references/completion-handoff.md +1 -1
- package/skills/peaks-code/references/skill-presence-and-title.md +1 -1
- package/dist/cli/commands/sub-agent-exec-command.d.ts +0 -25
- package/dist/cli/commands/sub-agent-exec-command.js +0 -132
- package/dist/services/standards/find-skills-integration.d.ts +0 -59
- package/dist/services/standards/find-skills-integration.js +0 -225
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `peaks sub-agent exec` — slice 4.0.7-PR-meta-6.
|
|
3
|
-
*
|
|
4
|
-
* Companion to `peaks sub-agent dispatch` (slice 2026-06-07). The
|
|
5
|
-
* dispatch CLI is dry-run by design (per skill-first / CLI-auxiliary
|
|
6
|
-
* red line PB-4): it returns a `toolCall` envelope that the LLM
|
|
7
|
-
* extracts args from and re-executes via the Agent tool. The 4.0.7
|
|
8
|
-
* ice-cola dogfood pass surfaced the friction: the LLM often
|
|
9
|
-
* skipped the dispatch altogether instead of paying the parse
|
|
10
|
-
* cost, and routed `Edit` calls directly. PR-meta-4 lowered the
|
|
11
|
-
* friction by adding `--emit-bash-script`, which prints a
|
|
12
|
-
* ready-to-exec bash script alongside the JSON envelope. PR-meta-6
|
|
13
|
-
* ships the actual surface the script invokes: this `exec`
|
|
14
|
-
* sub-command reads the dispatch record from disk (the same
|
|
15
|
-
* record the dispatch CLI wrote) and re-emits the canonical
|
|
16
|
-
* toolCall envelope so the LLM does not have to hand-parse the
|
|
17
|
-
* JSON to retry a dispatch.
|
|
18
|
-
*
|
|
19
|
-
* Architecture (PR-meta-6):
|
|
20
|
-
* 1. The user (or the LLM via Bash tool) runs the script
|
|
21
|
-
* emitted by `peaks sub-agent dispatch --emit-bash-script`.
|
|
22
|
-
* 2. The script invokes `peaks sub-agent exec --session-id ...
|
|
23
|
-
* --request-id ... --role ... --batch-id ... --dispatch-record ...
|
|
24
|
-
* --project ...`.
|
|
25
|
-
* 3. `exec` reads the dispatch record from disk, re-emits the
|
|
26
|
-
* same toolCall envelope, and prints a single next-action
|
|
27
|
-
* line: "Run this Task tool call to dispatch."
|
|
28
|
-
*
|
|
29
|
-
* The dry-run architecture is preserved: the LLM still has the
|
|
30
|
-
* final say on whether to actually spawn the Task tool. The
|
|
31
|
-
* script + exec pair is a transparent retry path, not a side-step.
|
|
32
|
-
*/
|
|
33
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
34
|
-
import { dirname } from 'node:path';
|
|
35
|
-
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
36
|
-
import { fail, ok } from 'peaks-loop-shared/result';
|
|
37
|
-
function failExec(command, message, options, hint) {
|
|
38
|
-
printResult(undefined, // io is plumbed below
|
|
39
|
-
fail(command, 'SUB_AGENT_EXEC_FAILED', message, null, [hint]), options.json);
|
|
40
|
-
// The above `printResult` expects io — the real call site passes
|
|
41
|
-
// io explicitly. This helper is only used for type-level
|
|
42
|
-
// documentation; the actual printing is inline in the action.
|
|
43
|
-
throw new Error(message);
|
|
44
|
-
}
|
|
45
|
-
export function registerSubAgentExecCommand(parent, io) {
|
|
46
|
-
addJsonOption(parent
|
|
47
|
-
.command('exec')
|
|
48
|
-
.description('Slice 4.0.7-PR-meta-6. Re-emit a stored dispatch envelope. Companion to `peaks sub-agent dispatch --emit-bash-script`. Reads the dispatch record from `--dispatch-record` and prints the canonical toolCall envelope so the LLM does not have to hand-parse the JSON to retry. Dry-run: the LLM still owns the actual Agent tool invocation.')
|
|
49
|
-
.requiredOption('--session-id <sid>', 'session id the dispatch was created in')
|
|
50
|
-
.requiredOption('--request-id <rid>', 'request id the dispatch was created for')
|
|
51
|
-
.requiredOption('--role <role>', 'sub-agent role (rd | qa | ui | sc | txt | qa-business | qa-business-api)')
|
|
52
|
-
.requiredOption('--batch-id <uuid>', 'batch id the dispatch was created in')
|
|
53
|
-
.requiredOption('--dispatch-record <path>', 'path to the dispatch record JSON (the same file the dispatch CLI wrote under .peaks/_sub_agents/<sid>/)')
|
|
54
|
-
.requiredOption('--project <path>', 'target project root (echoed back in the envelope; not used to read files)')).action((options) => {
|
|
55
|
-
if (options.dispatchRecord === undefined || !existsSync(options.dispatchRecord)) {
|
|
56
|
-
printResult(io, fail('sub-agent.exec', 'DISPATCH_RECORD_MISSING', `Dispatch record not found at ${options.dispatchRecord ?? '(unset)'}`, null, [
|
|
57
|
-
'Re-run `peaks sub-agent dispatch <role> --prompt <text> --request-id <rid> --json` to create the record.',
|
|
58
|
-
'Then pass the emitted `dispatchRecordPath` to `peaks sub-agent exec --dispatch-record <path>`.'
|
|
59
|
-
]), options.json);
|
|
60
|
-
process.exitCode = 1;
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
let record;
|
|
64
|
-
try {
|
|
65
|
-
record = JSON.parse(readFileSync(options.dispatchRecord, 'utf8'));
|
|
66
|
-
}
|
|
67
|
-
catch (err) {
|
|
68
|
-
printResult(io, fail('sub-agent.exec', 'DISPATCH_RECORD_INVALID', `Failed to parse dispatch record: ${err instanceof Error ? err.message : String(err)}`, { dispatchRecord: options.dispatchRecord }, [
|
|
69
|
-
'The dispatch record may be truncated. Re-run `peaks sub-agent dispatch` to create a fresh record.'
|
|
70
|
-
]), options.json);
|
|
71
|
-
process.exitCode = 1;
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
if (record.toolCall === undefined) {
|
|
75
|
-
printResult(io, fail('sub-agent.exec', 'DISPATCH_RECORD_NO_TOOL_CALL', 'Dispatch record has no `toolCall` field. The record may be from a pre-toolCall era; re-run `peaks sub-agent dispatch` to create a fresh record.', { dispatchRecord: options.dispatchRecord }, [
|
|
76
|
-
'Re-run `peaks sub-agent dispatch` to create a fresh record with a `toolCall` envelope.'
|
|
77
|
-
]), options.json);
|
|
78
|
-
process.exitCode = 1;
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
// Sanity check: the record's sessionId / rid / batchId / role
|
|
82
|
-
// should match the CLI flags (if the caller passed them). This
|
|
83
|
-
// is defensive: a mismatch usually means the operator is
|
|
84
|
-
// running the wrong script. We surface a warning but still
|
|
85
|
-
// re-emit the envelope (do not block — the dispatch record
|
|
86
|
-
// is the canonical source of truth).
|
|
87
|
-
const warnings = [];
|
|
88
|
-
if (options.sessionId !== undefined && record.sessionId !== undefined && record.sessionId !== options.sessionId) {
|
|
89
|
-
warnings.push(`session-id mismatch: record=${record.sessionId} vs --session-id=${options.sessionId}; using the record's value as the canonical source.`);
|
|
90
|
-
}
|
|
91
|
-
if (options.requestId !== undefined && record.rid !== undefined && record.rid !== options.requestId) {
|
|
92
|
-
warnings.push(`request-id mismatch: record=${record.rid} vs --request-id=${options.requestId}; using the record's value.`);
|
|
93
|
-
}
|
|
94
|
-
if (options.batchId !== undefined && record.batchId !== undefined && record.batchId !== options.batchId) {
|
|
95
|
-
warnings.push(`batch-id mismatch: record=${record.batchId} vs --batch-id=${options.batchId}; using the record's value.`);
|
|
96
|
-
}
|
|
97
|
-
if (options.role !== undefined && record.role !== undefined && record.role !== options.role) {
|
|
98
|
-
warnings.push(`role mismatch: record=${record.role} vs --role=${options.role}; using the record's value.`);
|
|
99
|
-
}
|
|
100
|
-
const nextActions = [
|
|
101
|
-
'This is a dry-run re-emit. Run the Task tool with the printed `toolCall.args` to actually dispatch the sub-agent.',
|
|
102
|
-
`After dispatching, the sub-agent should call \`peaks sub-agent heartbeat --record ${options.dispatchRecord}\` periodically.`
|
|
103
|
-
];
|
|
104
|
-
// Re-emit the same envelope shape as `peaks sub-agent dispatch`
|
|
105
|
-
// so consumers (LLM or downstream tooling) can treat both CLIs
|
|
106
|
-
// as the same surface. Slice 4.0.7-PR-meta-6: the envelope
|
|
107
|
-
// version is bumped to 2.4.0 to mark the new `viaExec` field
|
|
108
|
-
// (true here; false in dispatch CLI output).
|
|
109
|
-
printResult(io, ok('sub-agent.exec', {
|
|
110
|
-
envelopeVersion: '2.4.0',
|
|
111
|
-
viaExec: true,
|
|
112
|
-
sourceDispatchRecord: options.dispatchRecord,
|
|
113
|
-
role: record.role ?? options.role,
|
|
114
|
-
sessionId: record.sessionId ?? options.sessionId,
|
|
115
|
-
rid: record.rid ?? options.requestId,
|
|
116
|
-
batchId: record.batchId ?? options.batchId,
|
|
117
|
-
promptSize: record.promptSize ?? 0,
|
|
118
|
-
originalPromptSize: record.originalPromptSize ?? 0,
|
|
119
|
-
toolCall: record.toolCall
|
|
120
|
-
}, warnings, nextActions), options.json);
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
// Helper export so tests can drive the underlying logic without
|
|
124
|
-
// invoking the action handler. The action handler is the
|
|
125
|
-
// integration path; the helper is the unit path.
|
|
126
|
-
export function readDispatchRecordForExec(dispatchRecordPath) {
|
|
127
|
-
if (!existsSync(dispatchRecordPath)) {
|
|
128
|
-
throw new Error(`DISPATCH_RECORD_MISSING: ${dispatchRecordPath}`);
|
|
129
|
-
}
|
|
130
|
-
return JSON.parse(readFileSync(dispatchRecordPath, 'utf8'));
|
|
131
|
-
}
|
|
132
|
-
void dirname;
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import type { ProjectContext } from './project-context.js';
|
|
2
|
-
export interface SkillRecommendation {
|
|
3
|
-
/** GitHub owner/repo, e.g. "vercel-labs/agent-skills". */
|
|
4
|
-
readonly owner: string;
|
|
5
|
-
/** Skill slug within the repo, e.g. "react-best-practices". */
|
|
6
|
-
readonly skillSlug: string;
|
|
7
|
-
/** Display name (best-effort; may equal slug). */
|
|
8
|
-
readonly displayName: string;
|
|
9
|
-
/** Short description from the registry; empty when not parseable. */
|
|
10
|
-
readonly description: string;
|
|
11
|
-
/** Install count from the registry listing; null when unparseable. */
|
|
12
|
-
readonly installCount: number | null;
|
|
13
|
-
/** The shell command the user can paste to install this skill. */
|
|
14
|
-
readonly installCommand: string;
|
|
15
|
-
}
|
|
16
|
-
export interface FindSkillsResult {
|
|
17
|
-
readonly recommendations: readonly SkillRecommendation[];
|
|
18
|
-
/** Queries that found zero candidates (operator-visible). */
|
|
19
|
-
readonly emptyQueries: readonly string[];
|
|
20
|
-
/** `npx skills find` runs that failed (network / registry); non-fatal. */
|
|
21
|
-
readonly failedRuns: readonly {
|
|
22
|
-
readonly query: string;
|
|
23
|
-
readonly reason: string;
|
|
24
|
-
}[];
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Map a ProjectContext to a small query set. Returns at most 8
|
|
28
|
-
* queries to keep the network surface bounded; ordering is
|
|
29
|
-
* best-to-worst (more specific first, then broad). Deliberately
|
|
30
|
-
* no hardcoded owner whitelist — `findSkillsForContext` runs
|
|
31
|
-
* `npx skills find <query>` with no `--owner` filter so the
|
|
32
|
-
* registry / leaderboard surfaces whatever the user might want
|
|
33
|
-
* (per find-skills SKILL.md Step 2-3 guidance).
|
|
34
|
-
*/
|
|
35
|
-
export declare function queriesForContext(ctx: ProjectContext): readonly string[];
|
|
36
|
-
/**
|
|
37
|
-
* Spawn runner injection point. The default spawns
|
|
38
|
-
* `npx skills find <query>` (no `--owner` filter, so the registry
|
|
39
|
-
* decides what surfaces). Tests pass a deterministic stub.
|
|
40
|
-
*/
|
|
41
|
-
export type SkillFindRunner = (query: string) => Promise<string>;
|
|
42
|
-
export declare const defaultSkillFindRunner: SkillFindRunner;
|
|
43
|
-
/**
|
|
44
|
-
* Parse a `npx skills find` stdout blob into SkillRecommendation rows.
|
|
45
|
-
* The CLI output is line-oriented; a row looks like
|
|
46
|
-
* <owner>/<repo>@<slug> <install-count> <description>
|
|
47
|
-
* The parser is lenient — any line that contains a
|
|
48
|
-
* `<owner>/<repo>@<slug>` triple is captured.
|
|
49
|
-
*/
|
|
50
|
-
export declare function parseFindOutput(stdout: string): readonly SkillRecommendation[];
|
|
51
|
-
/**
|
|
52
|
-
* Main entry: query the registry for each query, dedup, sort by
|
|
53
|
-
* install-count desc (the registry's leaderboard already orders by
|
|
54
|
-
* popularity; we keep the same ordering). Returns top-N rows.
|
|
55
|
-
*/
|
|
56
|
-
export declare function findSkillsForContext(ctx: ProjectContext, options?: {
|
|
57
|
-
readonly topN?: number;
|
|
58
|
-
readonly findRunner?: SkillFindRunner;
|
|
59
|
-
}): Promise<FindSkillsResult>;
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Slice 4.0.7-PR-12 (ice-cola surface probe 2026-08-02).
|
|
3
|
-
*
|
|
4
|
-
* After `peaks standards init` / `peaks standards update` materializes
|
|
5
|
-
* the project-local standards tree, the user often wants to also
|
|
6
|
-
* bring in matching skills from the open agent-skills ecosystem
|
|
7
|
-
* (https://skills.sh/). A TypeScript monorepo with React + NestJS +
|
|
8
|
-
* Tailwind + Playwright + pnpm has natural companions in
|
|
9
|
-
* vercel-labs/agent-skills, anthropics/skills, and similar curated
|
|
10
|
-
* collections.
|
|
11
|
-
*
|
|
12
|
-
* Slice 4.0.7 dogfood round 2 (user direction 2026-08-02): do NOT
|
|
13
|
-
* hardcode a `VERIFIED_SKILL_OWNERS` whitelist. The first version
|
|
14
|
-
* shipped a static list of "trusted" owners, but that is exactly
|
|
15
|
-
* what the find-skills skill tells users NOT to do — let the
|
|
16
|
-
* registry + leaderboard decide. This service now:
|
|
17
|
-
* 1. maps the detected ProjectContext to a small set of natural
|
|
18
|
-
* language queries (component library, build tool, language).
|
|
19
|
-
* 2. runs `npx skills find <query>` ONCE per query with no
|
|
20
|
-
* `--owner` filter, so the registry returns whatever the
|
|
21
|
-
* leaderboard surfaces (vercel-labs/agent-skills at the top
|
|
22
|
-
* for "react" is a registry result, not a hardcoded one).
|
|
23
|
-
* 3. parses + dedups + ranks by install count desc (the
|
|
24
|
-
* registry / leaderboard already orders by popularity).
|
|
25
|
-
* 4. emits a `SkillRecommendation` list with `install` command.
|
|
26
|
-
*
|
|
27
|
-
* **Hard rule (per peaks-loop external-references):** this service
|
|
28
|
-
* NEVER calls `npx skills add` itself. Installation is gated on the
|
|
29
|
-
* user passing `--suggest-skills` to `peaks standards init/update`,
|
|
30
|
-
* and the install command is emitted as a single shell snippet the
|
|
31
|
-
* user can review before pasting. Default behavior: recommendation
|
|
32
|
-
* only, no install. The CLI caller owns the gating.
|
|
33
|
-
*
|
|
34
|
-
* Pure: no I/O of the project tree; only `npx skills find` calls
|
|
35
|
-
* (which are external side effects that we treat as testable via a
|
|
36
|
-
* `skillFindRunner` injection).
|
|
37
|
-
*/
|
|
38
|
-
import { spawn } from 'node:child_process';
|
|
39
|
-
/**
|
|
40
|
-
* Map a ProjectContext to a small query set. Returns at most 8
|
|
41
|
-
* queries to keep the network surface bounded; ordering is
|
|
42
|
-
* best-to-worst (more specific first, then broad). Deliberately
|
|
43
|
-
* no hardcoded owner whitelist — `findSkillsForContext` runs
|
|
44
|
-
* `npx skills find <query>` with no `--owner` filter so the
|
|
45
|
-
* registry / leaderboard surfaces whatever the user might want
|
|
46
|
-
* (per find-skills SKILL.md Step 2-3 guidance).
|
|
47
|
-
*/
|
|
48
|
-
export function queriesForContext(ctx) {
|
|
49
|
-
const queries = [];
|
|
50
|
-
// Component library first (most specific)
|
|
51
|
-
if (ctx.componentLibrary.name === 'antd')
|
|
52
|
-
queries.push('antd', 'react');
|
|
53
|
-
else if (ctx.componentLibrary.name === 'antd-pro')
|
|
54
|
-
queries.push('antd-pro', 'react', 'pro-components');
|
|
55
|
-
else if (ctx.componentLibrary.name === 'mui')
|
|
56
|
-
queries.push('mui', 'react');
|
|
57
|
-
else if (ctx.componentLibrary.name === 'shadcn')
|
|
58
|
-
queries.push('shadcn', 'react', 'tailwind');
|
|
59
|
-
else if (ctx.componentLibrary.name === 'arco')
|
|
60
|
-
queries.push('arco', 'react');
|
|
61
|
-
else if (ctx.componentLibrary.name === 'tdesign')
|
|
62
|
-
queries.push('tdesign', 'react');
|
|
63
|
-
else if (ctx.componentLibrary.name === 'element-plus')
|
|
64
|
-
queries.push('element-plus', 'vue');
|
|
65
|
-
else if (ctx.componentLibrary.name === 'vant')
|
|
66
|
-
queries.push('vant', 'mobile');
|
|
67
|
-
// Build tool
|
|
68
|
-
if (ctx.buildTool === 'next')
|
|
69
|
-
queries.push('nextjs', 'react');
|
|
70
|
-
else if (ctx.buildTool === 'vite')
|
|
71
|
-
queries.push('vite', 'frontend');
|
|
72
|
-
else if (ctx.buildTool === 'umi')
|
|
73
|
-
queries.push('umi');
|
|
74
|
-
else if (ctx.buildTool === 'rsbuild')
|
|
75
|
-
queries.push('rsbuild', 'rspack');
|
|
76
|
-
else if (ctx.buildTool === 'rspack')
|
|
77
|
-
queries.push('rspack');
|
|
78
|
-
// CSS framework
|
|
79
|
-
if (ctx.cssFrameworks.includes('tailwind'))
|
|
80
|
-
queries.push('tailwind');
|
|
81
|
-
if (ctx.cssFrameworks.includes('less'))
|
|
82
|
-
queries.push('less');
|
|
83
|
-
// Domain hints
|
|
84
|
-
for (const dep of ctx.notableDeps) {
|
|
85
|
-
if (dep.startsWith('@nestjs/'))
|
|
86
|
-
queries.push('nestjs');
|
|
87
|
-
if (dep === 'playwright' || dep === '@playwright/test')
|
|
88
|
-
queries.push('playwright', 'e2e');
|
|
89
|
-
if (dep === 'vitest')
|
|
90
|
-
queries.push('vitest', 'testing');
|
|
91
|
-
if (dep === 'prisma')
|
|
92
|
-
queries.push('prisma');
|
|
93
|
-
if (dep === 'drizzle-orm')
|
|
94
|
-
queries.push('drizzle');
|
|
95
|
-
if (dep === 'graphql' || dep === '@apollo/client')
|
|
96
|
-
queries.push('graphql');
|
|
97
|
-
}
|
|
98
|
-
// Always include the language + the platform itself
|
|
99
|
-
queries.push(ctx.hasPackageJson ? 'typescript' : 'javascript');
|
|
100
|
-
// Dedup while preserving order, then cap at 12 to bound the
|
|
101
|
-
// network surface (each query fans out to the registry). 12
|
|
102
|
-
// keeps both "specific" (antd, react, nestjs) and "broad"
|
|
103
|
-
// (typescript / javascript) queries in the same list.
|
|
104
|
-
const seen = new Set();
|
|
105
|
-
const deduped = queries.filter((q) => (seen.has(q) ? false : (seen.add(q), true)));
|
|
106
|
-
return deduped.slice(0, 12);
|
|
107
|
-
}
|
|
108
|
-
export const defaultSkillFindRunner = (query) => new Promise((resolveText, reject) => {
|
|
109
|
-
// No `--owner` flag: the registry's leaderboard decides the
|
|
110
|
-
// ranking. We do NOT hardcode a "verified owners" whitelist —
|
|
111
|
-
// that is exactly the anti-pattern the find-skills SKILL.md
|
|
112
|
-
// warns against (Step 4: verify install count + source
|
|
113
|
-
// reputation, but trust the leaderboard first).
|
|
114
|
-
const child = spawn('npx', ['skills', 'find', query], {
|
|
115
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
116
|
-
});
|
|
117
|
-
let stdout = '';
|
|
118
|
-
let stderr = '';
|
|
119
|
-
child.stdout.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
|
|
120
|
-
child.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
|
|
121
|
-
child.on('error', reject);
|
|
122
|
-
child.on('close', (code) => {
|
|
123
|
-
if (code === 0)
|
|
124
|
-
resolveText(stdout);
|
|
125
|
-
else
|
|
126
|
-
reject(new Error(`npx skills find exited ${code}: ${stderr.slice(0, 200)}`));
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
/**
|
|
130
|
-
* Parse a `npx skills find` stdout blob into SkillRecommendation rows.
|
|
131
|
-
* The CLI output is line-oriented; a row looks like
|
|
132
|
-
* <owner>/<repo>@<slug> <install-count> <description>
|
|
133
|
-
* The parser is lenient — any line that contains a
|
|
134
|
-
* `<owner>/<repo>@<slug>` triple is captured.
|
|
135
|
-
*/
|
|
136
|
-
export function parseFindOutput(stdout) {
|
|
137
|
-
const lines = stdout.split('\n');
|
|
138
|
-
const rows = [];
|
|
139
|
-
const rowRe = /([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)@([A-Za-z0-9_.-]+)/;
|
|
140
|
-
for (const line of lines) {
|
|
141
|
-
const m = line.match(rowRe);
|
|
142
|
-
if (!m || m[1] === undefined || m[2] === undefined || m[3] === undefined)
|
|
143
|
-
continue;
|
|
144
|
-
const rowOwner = m[1];
|
|
145
|
-
const repoName = m[2];
|
|
146
|
-
const skillSlug = m[3];
|
|
147
|
-
// Parse install count if the line has a number with K/M suffix.
|
|
148
|
-
let installCount = null;
|
|
149
|
-
const countMatch = line.match(/(\d+(?:\.\d+)?)\s*([KkMm]?)\b/);
|
|
150
|
-
if (countMatch && countMatch[1] !== undefined) {
|
|
151
|
-
const base = parseFloat(countMatch[1]);
|
|
152
|
-
const suffix = (countMatch[2] ?? '').toUpperCase();
|
|
153
|
-
if (!Number.isNaN(base)) {
|
|
154
|
-
installCount = suffix === 'K' ? base * 1_000 : suffix === 'M' ? base * 1_000_000 : base;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
const desc = line.replace(rowRe, '').trim();
|
|
158
|
-
const fullSlug = `${rowOwner}/${repoName}@${skillSlug}`;
|
|
159
|
-
rows.push({
|
|
160
|
-
owner: rowOwner,
|
|
161
|
-
skillSlug,
|
|
162
|
-
displayName: skillSlug,
|
|
163
|
-
description: desc,
|
|
164
|
-
installCount,
|
|
165
|
-
installCommand: `npx skills add ${fullSlug} -g -y`,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
return rows;
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Main entry: query the registry for each query, dedup, sort by
|
|
172
|
-
* install-count desc (the registry's leaderboard already orders by
|
|
173
|
-
* popularity; we keep the same ordering). Returns top-N rows.
|
|
174
|
-
*/
|
|
175
|
-
export async function findSkillsForContext(ctx, options = {}) {
|
|
176
|
-
const topN = options.topN ?? 8;
|
|
177
|
-
const findRunner = options.findRunner ?? defaultSkillFindRunner;
|
|
178
|
-
const queries = queriesForContext(ctx);
|
|
179
|
-
const seen = new Set();
|
|
180
|
-
const all = [];
|
|
181
|
-
const emptyQueries = [];
|
|
182
|
-
const failedRuns = [];
|
|
183
|
-
for (const query of queries) {
|
|
184
|
-
try {
|
|
185
|
-
const stdout = await findRunner(query);
|
|
186
|
-
const rows = parseFindOutput(stdout);
|
|
187
|
-
if (rows.length === 0) {
|
|
188
|
-
emptyQueries.push(query);
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
for (const r of rows) {
|
|
192
|
-
const key = `${r.owner}/${r.skillSlug}`;
|
|
193
|
-
if (seen.has(key))
|
|
194
|
-
continue;
|
|
195
|
-
seen.add(key);
|
|
196
|
-
all.push(r);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
catch (err) {
|
|
200
|
-
failedRuns.push({
|
|
201
|
-
query,
|
|
202
|
-
reason: err instanceof Error ? err.message : String(err),
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
// Sort by install count desc (registry leaderboard is already
|
|
207
|
-
// ordered by popularity; we keep the same order so the user sees
|
|
208
|
-
// the same top results they would see in `npx skills find`).
|
|
209
|
-
// Tiebreaker: display name asc.
|
|
210
|
-
all.sort((a, b) => {
|
|
211
|
-
if (a.installCount !== null && b.installCount !== null && a.installCount !== b.installCount) {
|
|
212
|
-
return b.installCount - a.installCount;
|
|
213
|
-
}
|
|
214
|
-
if (a.installCount === null && b.installCount !== null)
|
|
215
|
-
return 1;
|
|
216
|
-
if (a.installCount !== null && b.installCount === null)
|
|
217
|
-
return -1;
|
|
218
|
-
return a.displayName.localeCompare(b.displayName);
|
|
219
|
-
});
|
|
220
|
-
return {
|
|
221
|
-
recommendations: all.slice(0, topN),
|
|
222
|
-
emptyQueries,
|
|
223
|
-
failedRuns,
|
|
224
|
-
};
|
|
225
|
-
}
|