-
Notifications
You must be signed in to change notification settings - Fork 189
Support annotate command and add SKILL.md #122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
c7dadc3
c050733
04b9eaa
935ee6b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,185 @@ | ||||||||||||
| --- | ||||||||||||
| name: get-mslearn-docs | ||||||||||||
| description: > | ||||||||||||
| Use this skill when you need documentation from Microsoft Learn before writing | ||||||||||||
| code — for example, "use Azure Functions", "configure Cosmos DB", "set up App | ||||||||||||
| Service", or any time the user asks you to work with a Microsoft technology | ||||||||||||
| and you need current reference material. Fetch the docs with mslearn before | ||||||||||||
| answering, rather than relying on training knowledge. | ||||||||||||
| --- | ||||||||||||
|
|
||||||||||||
|
||||||||||||
| > NOTE: For repository tooling and validation to recognize this skill, the | |
| > canonical location for this file is `skills/get-mslearn-docs/SKILL.md`. | |
| > Move or copy this content there and update any references accordingly. |
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Step 1 says to review results for contentUrl, but then it instructs the reader to pick an id like openai/chat / stripe/api, which appears unrelated to this CLI and inconsistent with the mslearn search payload shape. Consider removing the id guidance and focusing on selecting the appropriate contentUrl from the search results.
| Review the search results to find the most relevant document `contentUrl`. If nothing matches, try a broader term. | |
| Pick the best-matching `id` from the results (e.g. `openai/chat`, `anthropic/sdk`, | |
| `stripe/api`). If nothing matches, try a broader term. | |
| Review the search results to find the most relevant document. Use its `contentUrl` in the next step. If nothing matches, try a broader or alternative search term. |
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The “Quick reference” table is not valid Markdown table syntax (it uses || prefixes and has extra blank | lines), so it likely won’t render as a table. Consider rewriting it using standard Markdown table formatting (| Goal | Command | header + separator row + data rows).
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import { Command } from 'commander'; | ||
|
|
||
| import type { CliContext } from '../context.js'; | ||
| import { normalizeUrl } from '../utils/options.js'; | ||
| import { ensureTrailingNewline } from '../utils/text.js'; | ||
| import { UsageError } from '../utils/errors.js'; | ||
|
|
||
| interface AnnotateCommandOptions { | ||
| clear?: boolean; | ||
| list?: boolean; | ||
| } | ||
|
|
||
| export function registerAnnotateCommand(program: Command, context: CliContext): void { | ||
| program | ||
| .command('annotate') | ||
| .description('Attach a local note to a Microsoft Learn URL. Notes persist across sessions.') | ||
| .argument('[url]', 'Microsoft Learn document URL.') | ||
| .argument('[note]', 'Note text to attach to the URL.') | ||
| .option('--clear', 'Remove the annotation for this URL.') | ||
| .option('--list', 'List all saved annotations.') | ||
| .action((url: string | undefined, note: string | undefined, options: AnnotateCommandOptions) => { | ||
| const store = context.createAnnotationStore(); | ||
|
|
||
| if (options.list) { | ||
| const annotations = store.list(); | ||
| if (annotations.length === 0) { | ||
| context.writeOut(ensureTrailingNewline('No annotations.')); | ||
| return; | ||
| } | ||
| const lines: string[] = []; | ||
| for (const a of annotations) { | ||
| lines.push(`${a.url} (${a.updatedAt})`); | ||
| lines.push(` ${a.note}`); | ||
| lines.push(''); | ||
| } | ||
| context.writeOut(ensureTrailingNewline(lines.join('\n'))); | ||
| return; | ||
| } | ||
|
|
||
| if (!url) { | ||
| throw new UsageError( | ||
| 'Missing required argument: <url>. Usage: mslearn annotate <url> <note> | mslearn annotate <url> --clear | mslearn annotate --list', | ||
| ); | ||
| } | ||
|
|
||
| const normalizedUrl = normalizeUrl(url); | ||
|
|
||
| if (options.clear) { | ||
| const removed = store.clear(normalizedUrl); | ||
| if (removed) { | ||
| context.writeOut(ensureTrailingNewline(`Annotation cleared for ${normalizedUrl}.`)); | ||
| } else { | ||
| context.writeOut(ensureTrailingNewline(`No annotation found for ${normalizedUrl}.`)); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| if (!note) { | ||
| const existing = store.read(normalizedUrl); | ||
| if (existing) { | ||
| context.writeOut(ensureTrailingNewline(`${existing.url} (${existing.updatedAt})\n${existing.note}`)); | ||
| } else { | ||
| context.writeOut(ensureTrailingNewline(`No annotation for ${normalizedUrl}.`)); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| const annotation = store.write(normalizedUrl, note); | ||
| context.writeOut(ensureTrailingNewline(`Annotation saved for ${annotation.url}.`)); | ||
| }); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,95 @@ | ||||||||||||||||||||||
| import { mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs'; | ||||||||||||||||||||||
| import { join } from 'node:path'; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import envPaths from 'env-paths'; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export interface Annotation { | ||||||||||||||||||||||
| url: string; | ||||||||||||||||||||||
| note: string; | ||||||||||||||||||||||
| updatedAt: string; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export interface AnnotationStore { | ||||||||||||||||||||||
| read(url: string): Annotation | undefined; | ||||||||||||||||||||||
| write(url: string, note: string): Annotation; | ||||||||||||||||||||||
| clear(url: string): boolean; | ||||||||||||||||||||||
| list(): Annotation[]; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| interface FileAnnotationStoreOptions { | ||||||||||||||||||||||
| annotationsDir?: string; | ||||||||||||||||||||||
| now?: () => number; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export function getDefaultAnnotationsDir(): string { | ||||||||||||||||||||||
| const paths = envPaths('mslearn', { suffix: '' }); | ||||||||||||||||||||||
| return join(paths.data, 'annotations'); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export function createFileAnnotationStore(options: FileAnnotationStoreOptions = {}): AnnotationStore { | ||||||||||||||||||||||
| return new FileAnnotationStore(options); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| function urlToFilename(url: string): string { | ||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. URL-to-filename collisions —
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Filename length limits — Long Microsoft Learn URLs can easily produce filenames exceeding the 255-character OS limit (Windows and most Linux filesystems), which would throw an |
||||||||||||||||||||||
| return url.replace(/[^a-zA-Z0-9.-]/g, '_') + '.json'; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| class FileAnnotationStore implements AnnotationStore { | ||||||||||||||||||||||
| private readonly annotationsDir: string; | ||||||||||||||||||||||
| private readonly now: () => number; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| constructor(options: FileAnnotationStoreOptions) { | ||||||||||||||||||||||
| this.annotationsDir = options.annotationsDir ?? getDefaultAnnotationsDir(); | ||||||||||||||||||||||
| this.now = options.now ?? Date.now; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| read(url: string): Annotation | undefined { | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| const filePath = join(this.annotationsDir, urlToFilename(url)); | ||||||||||||||||||||||
| const raw = readFileSync(filePath, 'utf8'); | ||||||||||||||||||||||
| return JSON.parse(raw) as Annotation; | ||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| write(url: string, note: string): Annotation { | ||||||||||||||||||||||
| mkdirSync(this.annotationsDir, { recursive: true }); | ||||||||||||||||||||||
| const annotation: Annotation = { | ||||||||||||||||||||||
| url, | ||||||||||||||||||||||
| note, | ||||||||||||||||||||||
| updatedAt: new Date(this.now()).toISOString(), | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
| const filePath = join(this.annotationsDir, urlToFilename(url)); | ||||||||||||||||||||||
| writeFileSync(filePath, JSON.stringify(annotation, null, 2), 'utf8'); | ||||||||||||||||||||||
| return annotation; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| clear(url: string): boolean { | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| const filePath = join(this.annotationsDir, urlToFilename(url)); | ||||||||||||||||||||||
| unlinkSync(filePath); | ||||||||||||||||||||||
| return true; | ||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||
| return false; | ||||||||||||||||||||||
|
||||||||||||||||||||||
| } catch { | |
| return false; | |
| } catch (err) { | |
| const error = err as NodeJS.ErrnoException; | |
| if (error && error.code === 'ENOENT') { | |
| // File does not exist: report as "not cleared" without treating as an error. | |
| return false; | |
| } | |
| // Surface other filesystem errors so callers aren't misled. | |
| throw err; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should update our existing skills instead of adding new skill