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.
Files changed (89) hide show
  1. package/CHANGELOG.md +46 -288
  2. package/dist/cli/commands/code-job-shape-commands.js +8 -11
  3. package/dist/cli/commands/code-mode-gate-commands.js +1 -43
  4. package/dist/cli/commands/code-runtime-commands.js +36 -14
  5. package/dist/cli/commands/core/skill-command.js +75 -13
  6. package/dist/cli/commands/core/standards-command.js +5 -50
  7. package/dist/cli/commands/dispatch-commands.d.ts +20 -0
  8. package/dist/cli/commands/dispatch-commands.js +39 -38
  9. package/dist/cli/commands/dispatch-from-dag.js +17 -0
  10. package/dist/cli/commands/heartbeat-commands.d.ts +21 -0
  11. package/dist/cli/commands/heartbeat-commands.js +40 -0
  12. package/dist/cli/commands/hook-handle.js +5 -2
  13. package/dist/cli/commands/hooks-commands.js +9 -26
  14. package/dist/cli/commands/loop-eval-commands.js +5 -2
  15. package/dist/cli/commands/sub-agent-commands.js +0 -2
  16. package/dist/cli/commands/sub-agent-shared.d.ts +7 -9
  17. package/dist/cli/commands/workflow-commands.js +8 -0
  18. package/dist/cli/commands/workflow-lifecycle-commands.d.ts +63 -0
  19. package/dist/cli/commands/workflow-lifecycle-commands.js +302 -0
  20. package/dist/cli/commands/workspace/init-command.js +19 -0
  21. package/dist/services/audit/backing-detector.d.ts +2 -19
  22. package/dist/services/audit/backing-detector.js +7 -66
  23. package/dist/services/audit/enforcers/active-skill-resolver.d.ts +25 -9
  24. package/dist/services/audit/enforcers/active-skill-resolver.js +83 -21
  25. package/dist/services/audit/enforcers/lint-audit-regression.js +1 -10
  26. package/dist/services/code/auto-compact-orchestrator.d.ts +35 -2
  27. package/dist/services/code/auto-compact-orchestrator.js +29 -9
  28. package/dist/services/context/build-dispatch-system-prompt.js +0 -18
  29. package/dist/services/dispatch/dispatch-record-writer.d.ts +25 -1
  30. package/dist/services/dispatch/dispatch-record-writer.js +85 -15
  31. package/dist/services/dispatch/test-tool-detection.d.ts +0 -14
  32. package/dist/services/dispatch/test-tool-detection.js +0 -22
  33. package/dist/services/doctor/doctor-service/checks/skill-presence.d.ts +8 -0
  34. package/dist/services/doctor/doctor-service/checks/skill-presence.js +9 -1
  35. package/dist/services/hooks/presence-marker-detector.d.ts +6 -0
  36. package/dist/services/hooks/presence-marker-detector.js +48 -0
  37. package/dist/services/ide/adapters/claude-code-adapter.js +20 -0
  38. package/dist/services/ide/adapters/codex-adapter.js +20 -1
  39. package/dist/services/ide/adapters/cursor-adapter.js +21 -1
  40. package/dist/services/ide/adapters/hermes-adapter.js +20 -1
  41. package/dist/services/ide/adapters/openclaw-adapter.js +20 -1
  42. package/dist/services/ide/adapters/qoder-adapter.js +20 -1
  43. package/dist/services/ide/adapters/tongyi-lingma-adapter.js +20 -1
  44. package/dist/services/ide/adapters/trae-adapter.js +21 -1
  45. package/dist/services/ide/adapters/zcode-adapter.js +19 -0
  46. package/dist/services/ide/ide-types.d.ts +15 -0
  47. package/dist/services/observability/observability-service.d.ts +2 -2
  48. package/dist/services/scan/archetype-service.js +2 -52
  49. package/dist/services/session/caller-binding-service.d.ts +21 -0
  50. package/dist/services/session/caller-binding-service.js +35 -0
  51. package/dist/services/session/caller-id-types.d.ts +62 -13
  52. package/dist/services/session/caller-id-types.js +25 -13
  53. package/dist/services/session/index.d.ts +4 -2
  54. package/dist/services/session/index.js +8 -2
  55. package/dist/services/session/platform-fallbacks.d.ts +25 -17
  56. package/dist/services/session/platform-fallbacks.js +26 -28
  57. package/dist/services/session/resolve-caller-id.d.ts +64 -29
  58. package/dist/services/session/resolve-caller-id.js +122 -48
  59. package/dist/services/session/session-manager.d.ts +0 -29
  60. package/dist/services/session/session-manager.js +0 -33
  61. package/dist/services/skills/hooks-settings-service.d.ts +0 -41
  62. package/dist/services/skills/hooks-settings-service.js +5 -61
  63. package/dist/services/skills/presence-lease-service.d.ts +73 -0
  64. package/dist/services/skills/presence-lease-service.js +358 -0
  65. package/dist/services/skills/presence-lease-types.d.ts +75 -0
  66. package/dist/services/skills/presence-lease-types.js +16 -0
  67. package/dist/services/skills/skill-presence-service.js +86 -22
  68. package/dist/services/slice/slice-check-service.js +0 -59
  69. package/dist/services/standards/project-standards-service.js +0 -44
  70. package/dist/services/workflow/workflow-graph-store.d.ts +77 -0
  71. package/dist/services/workflow/workflow-graph-store.js +278 -0
  72. package/dist/services/workflow/workflow-graph-types.d.ts +56 -0
  73. package/dist/services/workflow/workflow-graph-types.js +67 -0
  74. package/dist/services/workflow/workflow-inflight-probe.d.ts +52 -0
  75. package/dist/services/workflow/workflow-inflight-probe.js +86 -0
  76. package/dist/services/workflow/workflow-node-lifecycle.d.ts +67 -0
  77. package/dist/services/workflow/workflow-node-lifecycle.js +348 -0
  78. package/dist/services/workflow/workflow-presence-lifecycle.d.ts +58 -0
  79. package/dist/services/workflow/workflow-presence-lifecycle.js +196 -0
  80. package/dist/services/workspace/reconcile-service.d.ts +31 -0
  81. package/dist/services/workspace/reconcile-service.js +132 -1
  82. package/package.json +4 -4
  83. package/skills/peaks-code/SKILL.md +5 -43
  84. package/skills/peaks-code/references/completion-handoff.md +1 -1
  85. package/skills/peaks-code/references/skill-presence-and-title.md +1 -1
  86. package/dist/cli/commands/sub-agent-exec-command.d.ts +0 -25
  87. package/dist/cli/commands/sub-agent-exec-command.js +0 -132
  88. package/dist/services/standards/find-skills-integration.d.ts +0 -59
  89. 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
- }