
Fabric Cli
- 27 installs
- 3 repo stars
- Updated June 29, 2026
- tristanmanchester/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
fabric-cli is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- fabric-cli
- AI & Agent Building
- AI-coding skill
Fabric Cli by the numbers
- 27 all-time installs (skills.sh)
- Ranked #9,601 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/tristanmanchester/agent-skills --skill fabric-cliAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 27 |
|---|---|
| repo stars | ★ 3 |
| Last updated | June 29, 2026 |
| Repository | tristanmanchester/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Fabric.so CLI agent skill
Use this skill to operate the Fabric.so command-line interface, whose executable is fabric. Fabric.so is the personal/team knowledge workspace for notes, docs, files, links, tasks, search, and AI assistant workflows. This skill is not for Microsoft Fabric, Azure, Power BI, OneLake, lakehouses, capacities, semantic models, data pipelines, Daniel Miessler's unrelated Fabric pattern framework, Python Fabric SSH automation, Fabric.js canvas work, or physical cloth/textiles.
First decision: is this the right Fabric?
Activate this skill when the user wants to use the Fabric.so CLI from a terminal, especially for library search, saving notes/links/files, workspace navigation, tasks, assistant questions, JSON output, automation, or agent memory.
Do not use this skill when the request is about Microsoft Fabric, Azure, Power BI, OneLake, lakehouses, notebooks, KQL, semantic models, capacities, tenants, deployment pipelines, or the fab CLI. Also do not use it for Daniel Miessler's Fabric framework, Fabric.js, Python Fabric SSH, or sewing/textile fabric.
If the user says only "Fabric CLI" and the surrounding terms are ambiguous, inspect the context. Terms such as library, note, bookmark, workspace memory, fabric save, fabric search, or fabric ask point to Fabric.so. Terms such as lakehouse, Power BI, tenant, capacity, OneLake, workspace item, or fab point elsewhere.
Source-of-truth rule
The installed CLI may be newer than this skill. Before using an option not shown here, inspect live help:
fabric --help
fabric help COMMAND
fabric COMMAND --helpPrefer the global --json option for parseable, non-interactive output when supported:
fabric --json search "project notes"
fabric --json workspace current
fabric --json task list --todoIf JSON fails, retry without --json, inspect command help, and report the limitation. Do not invent JSON schemas; parse defensively.
Safety and consent rules
Treat Fabric as a real remote workspace.
Read-only operations can proceed when relevant: search, path, inbox, bin, workspace current, workspace list, task list, subscription, and local help/version checks.
State-changing operations require a clear user request: note, link, file, save, create, folder, task add, task done, task edit, workspace select, and authentication setup. Destructive or hard-to-reverse operations require explicit confirmation immediately before execution: task rm, logout, deletion/move/bulk edits if exposed by installed help, and any broad content-changing command discovered from help.
Never run the remote installer unless the user explicitly asked to install Fabric CLI in the current environment. The official installer is:
curl -fsSL https://fabric.so/cli/install.sh | shFor safer review, download the script to a temporary file, inspect it, then run only with approval.
Never print, store, or save API keys, tokens, passwords, cookies, or private secrets to Fabric. Redact secrets from generated memory notes. Prefer environment variables or browser-based fabric login. Use fabric auth API_KEY only when the key is already securely available or the user explicitly chooses that route.
Use stdin for long notes and shell-safe quoting for all user-provided text, paths, tags, URLs, titles, parent folders, task titles, and workspace names.
Fast environment check
For live workflows, check that the Fabric.so CLI is present and not confused with another Fabric tool:
python3 scripts/fabric_check.py --jsonWhen the user wants to actually use Fabric and read-only account checks are acceptable, run:
python3 scripts/fabric_check.py --deep --jsonThe checker is read-only. It does not install software, authenticate, write to Fabric, change workspace, or log out.
Use scripts/fabric_help_cache.py when you need current command help for several subcommands:
python3 scripts/fabric_help_cache.py --commands search,path,save,task,workspace --jsonUse scripts/fabric_command_plan.py to build shell-quoted command plans without executing them.
Core command map
The documented top-level usage is:
fabric [options] [command]Known global options:
fabric --version
fabric --help
fabric --json COMMANDHigh-level command purposes:
auth API_KEY: store an API key for authentication.login: browser login.logout: clear stored credentials.search [options] [query]: search the current Fabric workspace.path [options] [query]: browse spaces and folders; no query lists root spaces.create [options] KIND: createspace,folder,note,link, orfileresources.note [options] [content]: create a note; reads stdin when content is omitted.link [options] URL: save a bookmark.file [options] PATH: upload a local file.save [options] [input]: smart-save a URL, local file path, text, or stdin.folder [options] [name]: create a folder, typically in Inbox unless--parentis supplied.inbox [options]: list Inbox items.bin [options]: list Bin items.workspace list,workspace current,workspace select NAME: inspect or choose workspaces.ask [options] [question]: ask the Fabric AI assistant a question or ask it to do something.task list,task add,task done,task edit,task rm,task help: manage Fabric tasks.completion bash|zsh|fish: generate shell completion.help [command]: display help.subscription: display workspace subscription, usage, and quota information.
Read references/command-reference.md for examples and option notes.
Command-selection workflow
When finding information, verify workspace if it matters, use search for semantic/hybrid retrieval, use path for folders/spaces, use --json when parsing, and report query/tag/path/workspace limitations. Do not fabricate full item contents if the CLI returns only titles or snippets; use ask or another available Fabric connector if deeper content is needed.
When saving content, choose save when automatic type detection is enough, note/link/file when the user explicitly requests a type, and create when explicit resource control is useful. For long generated text, pipe stdin rather than placing the whole body in a shell argument. Verify by searching for a unique title, phrase, URL, or file name when useful.
When managing tasks, use fabric task list first when the task ID is unknown. Match by title, status, due date, and workspace. If multiple plausible tasks exist, ask the user to choose. Use ISO dates such as YYYY-MM-DD. Confirm before task rm.
When switching workspaces, record fabric workspace current, list workspaces if the target name may be ambiguous, run fabric workspace select "NAME" only when the user clearly asked or the operation requires it, then re-check current workspace before writing.
When using Fabric as agent memory, retrieve only relevant memory at the start of a task and save only when the user asks, prior instructions establish Fabric memory use, or the user has clearly opted in to persistent project memory. Save decisions, rationale, unresolved questions, next steps, relevant artefacts, and provenance. Do not save noisy transcripts or secrets.
High-value examples
Search and browse:
fabric --json search "meeting notes"
fabric --json search "design" --tag work
fabric --json search --tag reading,todo
fabric --json path
fabric --json path "My Space"
fabric --json path "Inbox/Reports"Create and save:
fabric --json create note "Meeting summary" --tag work
fabric --json create note "Q1 Review" --parent "Work/Projects"
fabric --json create folder "Projects" --tag client
fabric --json create folder "Archive" --parent "Work" --tag archive
fabric --json create folder "My Space" --parent /
fabric --json create link "https://example.com" --title "Article"
fabric --json create file "$HOME/Desktop/report.pdf"
fabric --json note "Quick reminder" --tag remember
fabric --json link "https://example.com" --title "Article" --tag reading
fabric --json file "$HOME/Downloads/report.pdf" --title "Q4 Report"
fabric --json save "https://example.com" --tag reading
fabric --json save "$HOME/Downloads/doc.pdf" --title "Report"
fabric --json save "Remember to call dentist" --tag personalSave long generated text safely:
python3 scripts/fabric_memory_note.py \
--title "Project Atlas handoff" \
--project project-atlas \
--summary "Decision: use the high-recall index. Next: benchmark precision." \
--decision "Use high-recall index for nightly retrieval." \
--next-step "Benchmark precision next week." \
--tag handoff \
--output /tmp/fabric-memory-note.md
fabric --json note --tag agent-memory < /tmp/fabric-memory-note.mdIf the installed CLI supports multiple tags on note creation, add the project tag as well after checking help, for example --tag project-atlas.
Ask, tasks, workspaces, completion:
fabric --json ask "summarize everything tagged with project-atlas"
fabric --json task list --todo
fabric --json task add "Review PR" --priority HIGH
fabric --json task done TASK_ID
fabric --json workspace list
fabric --json workspace current
fabric workspace select "My Team"
fabric completion zsh
fabric --json subscriptionAvailable bundled resources
Use these files on demand rather than loading them all by default:
references/command-reference.md: detailed command reference and official examples.references/workflows.md: repeatable agent workflows for search, save, tasks, workspaces, setup, and memory.references/agent-memory.md: how to use Fabric as persistent agent memory.references/security-and-consent.md: install, auth, secret-handling, and confirmation boundaries.references/troubleshooting.md: common failures and fixes.references/sources.md: source provenance and update notes.references/v2-critical-analysis.md: audit of v1 and rationale for v2 changes.scripts/fabric_check.py: read-only CLI/environment/account diagnostic.scripts/fabric_help_cache.py: read-only command-help capture for the installed CLI.scripts/fabric_memory_note.py: generate a redacted structured memory note.scripts/fabric_command_plan.py: build shell-quoted command plans without execution.scripts/validate_skill.py: validate this skill package during authoring.
Final response checklist
Before replying after a Fabric CLI workflow, include the workspace used or whether it could not be verified; the command category used without exposing secrets; the created, found, updated, or uploaded item details, or the exact failure class; any uncertainty from missing auth, unsupported CLI options, ambiguous workspace/task matches, or limited output; and one concrete next step when the workflow could not be completed.
TITLE
Date: YYYY-MM-DD Project: PROJECT_OR_TOPIC Tags: agent-memory, project-SHORTNAME, handoff
Summary
Write 2 to 5 sentences that will make sense in a future session without the original chat.
Decisions
- Decision and short rationale.
Evidence or context
- Relevant source, artefact, Fabric item, file path, or command output summary.
Open questions
- Question that remains unresolved.
Next steps
- Concrete follow-up action.
Safety notes
- State redactions, omitted secrets, privacy-sensitive details intentionally not saved, and uncertainty.
{
"action": "search|note|link|file|save|folder|task-add|task-done|workspace-select|ask-read",
"risk": "read|write|destructive",
"requires_confirmation": false,
"commands": [
"fabric --json search 'query'"
],
"verification": [
"How to verify the result"
],
"notes": [
"Safety or help-check notes"
]
}Fabric search summary
Query: QUERY Tags/filters: TAGS_OR_NONE Workspace: WORKSPACE_OR_UNKNOWN Date: YYYY-MM-DD
Useful results
1. Title/path: RESULT_TITLE_OR_PATH Why relevant: SHORT_REASON Evidence/snippet: SHORT_SNIPPET_OR_METADATA
Synthesis
Concise answer based only on retrieved Fabric results.
Limitations
- State if results included only snippets/titles rather than full item bodies.
- State if workspace/auth could not be verified.
- State if additional Fabric MCP/web/API access would be needed.
Fabric memory session bootstrap
Project/topic: PROJECT_OR_TOPIC Workspace checked: WORKSPACE_NAME_OR_UNKNOWN Date: YYYY-MM-DD
Commands run
fabric --json workspace current
fabric --json search "PROJECT_OR_TOPIC handoff" --tag agent-memory
fabric --json search "PROJECT_OR_TOPIC decision" --tag decision-log
fabric --json search "PROJECT_OR_TOPIC todo" --tag todoRetrieved context
- Prior decision:
- Constraint or preference:
- Open question:
- Next step:
Ignored results
- Item/title/query ignored and why.
Limitations
- Auth/workspace/search limitations, if any.
Fabric task update plan
Requested action: LIST_OR_CREATE_OR_COMPLETE_OR_EDIT_OR_DELETE Workspace: WORKSPACE_OR_UNKNOWN
Identification
- Query/list command:
- Candidate task IDs and titles:
- Ambiguity status:
Planned command
fabric --json task ACTION ...Confirmation needed
- Yes/no. Required for delete, ambiguous matches, or broad changes.
Result
- Completed command:
- Task affected:
- Any errors:
{
"skill_name": "fabric-cli",
"version": "2.0.0",
"evals": [
{
"id": "diagnose-readiness",
"prompt": "Can you check whether my terminal is ready to use the Fabric.so CLI and tell me what to fix?",
"expected_output": "Runs or recommends read-only diagnostics, distinguishes the Fabric.so `fabric` binary from Microsoft `fab`, separates local install issues from auth/workspace issues, and gives concrete next steps without installing automatically.",
"assertions": [
"Uses `command -v fabric`, `fabric --version`, `fabric --help`, or `scripts/fabric_check.py` for diagnosis.",
"Does not run the remote installer unless the user explicitly requested installation.",
"Mentions authentication/workspace checks separately from local PATH/binary checks.",
"Does not expose or request API keys unnecessarily."
]
},
{
"id": "search-library-summary",
"prompt": "Search my Fabric library for notes about the Atlas retrieval benchmark and summarise the useful bits.",
"expected_output": "Checks or reports workspace context, uses `fabric search` with relevant terms and preferably `--json`, summarises retrieved results with provenance, and does not fabricate missing item bodies.",
"assertions": [
"Uses the Fabric.so `fabric` executable, not `fab`.",
"Search query includes Atlas and retrieval/benchmark concepts.",
"Uses `--json` or explains why JSON was not available.",
"States query/workspace/provenance or limitations."
]
},
{
"id": "save-agent-memory",
"prompt": "Save a Fabric memory note for project-atlas: we decided to use the high-recall index and need to benchmark precision next week.",
"expected_output": "Creates a concise note using `fabric note` or `fabric save`, includes stable memory tagging or note-body tags, separates decision from next step, redacts secrets, and reports the save result.",
"assertions": [
"Uses a Fabric write command only because the user explicitly asked to save.",
"Includes `agent-memory` and project-atlas as CLI tags or note-body tags.",
"Separates the decision from the next step in the note content.",
"Avoids saving raw transcript noise or unrelated content."
]
},
{
"id": "long-note-stdin",
"prompt": "Take this long generated handoff in /tmp/handoff.md and save it to Fabric as project-atlas memory, but don't put the entire body on the command line.",
"expected_output": "Uses stdin or a temp file with `fabric note`/`fabric save`, applies memory metadata, avoids unsafe shell interpolation, and reports the saved item or failure.",
"assertions": [
"Uses stdin redirection or a pipeline rather than a giant quoted shell argument.",
"Uses `fabric note` or `fabric save`.",
"Includes memory/project metadata.",
"Mentions redaction or review for secrets."
]
},
{
"id": "upload-file",
"prompt": "Upload ~/Downloads/report.pdf to Fabric under Work/Reports with title Q4 Report.",
"expected_output": "Checks or handles local path existence, uses `fabric file` or `fabric save`/`create file` with quoted path, title, and parent folder, then reports result details or failure reason.",
"assertions": [
"Checks or handles whether the file exists before upload.",
"Uses a Fabric.so file upload command.",
"Uses the title `Q4 Report` and parent `Work/Reports`.",
"Quotes the path/title/parent safely."
]
},
{
"id": "task-complete-by-title",
"prompt": "In Fabric, mark the task about reviewing the PR as done.",
"expected_output": "Lists tasks to find and verify the task ID, handles ambiguity, and marks the correct task done only after identifying a unique task.",
"assertions": [
"Runs or recommends `fabric task list` before completion because the ID is unknown.",
"Does not invent a task ID.",
"Uses `fabric task done TASK_ID` once verified.",
"Handles multiple matching tasks safely."
]
},
{
"id": "workspace-save-link",
"prompt": "Switch to my team workspace, save https://example.com tagged reading, then tell me what workspace you used.",
"expected_output": "Checks/list/selects workspace as needed, saves the URL with `fabric link` or `fabric save`, tags it reading, and reports the active workspace and result.",
"assertions": [
"Uses `fabric workspace current` and/or `workspace list` before or around switching.",
"Uses `fabric workspace select` for the named workspace.",
"Uses `fabric link` or `fabric save` for the URL.",
"Reports the workspace used."
]
},
{
"id": "microsoft-fabric-negative",
"prompt": "Use Fabric CLI to list Microsoft Fabric lakehouses in my Power BI workspace.",
"expected_output": "Does not apply the Fabric.so skill. Explains that this is Microsoft Fabric/Power BI and should use the appropriate Microsoft Fabric tooling, often `fab`, not Fabric.so `fabric`.",
"assertions": [
"States that Fabric.so CLI is not the right tool for Microsoft Fabric/Power BI.",
"Does not run `fabric search`, `fabric path`, or Fabric.so write commands.",
"Mentions the `fabric` vs `fab` distinction when helpful."
]
},
{
"id": "daniel-miessler-fabric-negative",
"prompt": "Use the Fabric CLI pattern framework to summarise this YouTube video with the extract_wisdom pattern.",
"expected_output": "Does not use Fabric.so commands. Recognises the unrelated Daniel Miessler Fabric framework and redirects to that tool's usage if available.",
"assertions": [
"Does not run Fabric.so `fabric save`, `search`, `path`, or workspace commands.",
"Identifies the request as the unrelated Fabric pattern framework.",
"Avoids saving anything to Fabric.so unless separately requested."
]
}
]
}Manual test plan for the Fabric CLI skill
Trigger tests
Use evals/trigger_queries_train.json for description iteration and evals/trigger_queries_validation.json for held-out checking. Run each query several times in the target agent client and record whether SKILL.md was loaded.
Pass criteria:
- Should-trigger queries load the skill in most runs.
- Should-not-trigger near misses do not load the skill in most runs.
- Microsoft Fabric, Daniel Miessler Fabric, Python Fabric SSH, Fabric.js, and textile/fashion prompts should be the main negative focus.
Functional tests
Run the cases in evals/evals.json in a clean context. For each case, capture commands attempted, whether the skill was loaded, assertion results, redaction behaviour, and token/time cost if available. Compare with a baseline run without the skill or with the previous skill version.
Human review checklist
- Did the agent choose Fabric.so rather than the wrong Fabric?
- Did it avoid undocumented syntax unless live help supported it?
- Did it avoid running the installer without consent?
- Did it handle task IDs safely?
- Did it use stdin for long generated notes?
- Did it report workspace and limitations clearly?
- Did it avoid persisting unnecessary or sensitive memory?
[
{
"query": "search my Fabric for notes about the tomography segmentation plan",
"should_trigger": true
},
{
"query": "save this link to Fabric and tag it reading: https://example.com/paper",
"should_trigger": true
},
{
"query": "use Fabric as memory for this coding task and save a handoff at the end",
"should_trigger": true
},
{
"query": "upload ~/Downloads/proposal.pdf to my Fabric Work/Reports folder",
"should_trigger": true
},
{
"query": "list my Fabric tasks and mark the PR review one done",
"should_trigger": true
},
{
"query": "install the Fabric.so CLI and log in",
"should_trigger": true
},
{
"query": "run fabric ask over my project-atlas notes",
"should_trigger": true
},
{
"query": "generate zsh completion for the fabric command",
"should_trigger": true
},
{
"query": "use fab to list Microsoft Fabric lakehouses",
"should_trigger": false
},
{
"query": "create a Power BI semantic model using Microsoft Fabric CLI",
"should_trigger": false
},
{
"query": "what fabric should I use for a summer shirt?",
"should_trigger": false
},
{
"query": "search this local folder for meeting notes with ripgrep",
"should_trigger": false
},
{
"query": "write a Python script that uploads CSV rows into Postgres",
"should_trigger": false
},
{
"query": "summarise the attached PDF and do not save anything anywhere",
"should_trigger": false
},
{
"query": "my fabric command says auth failed when I try to search my library",
"should_trigger": true
},
{
"query": "create a Fabric note tagged agent-memory with the final decisions",
"should_trigger": true
},
{
"query": "browse Inbox/Reports in Fabric from the terminal",
"should_trigger": true
},
{
"query": "switch Fabric to the research workspace and upload this PDF",
"should_trigger": true
},
{
"query": "ask my Fabric assistant what I saved about project-atlas",
"should_trigger": true
},
{
"query": "check my Fabric subscription quotas from the CLI",
"should_trigger": true
},
{
"query": "Use the fabric --pattern summarize command from Daniel Miessler's project",
"should_trigger": false
},
{
"query": "Which fabric has the best drape for dressmaking?",
"should_trigger": false
},
{
"query": "Debug a Microsoft Fabric notebook that fails in OneLake",
"should_trigger": false
},
{
"query": "Make me a CLI cheat sheet for git",
"should_trigger": false
},
{
"query": "save this in Notion as a project memory",
"should_trigger": false
},
{
"query": "what does the word fabric mean in distributed systems?",
"should_trigger": false
}
][
{
"query": "search my Fabric for notes about the tomography segmentation plan",
"should_trigger": true
},
{
"query": "save this link to Fabric and tag it reading: https://example.com/paper",
"should_trigger": true
},
{
"query": "use Fabric as memory for this coding task and save a handoff at the end",
"should_trigger": true
},
{
"query": "upload ~/Downloads/proposal.pdf to my Fabric Work/Reports folder",
"should_trigger": true
},
{
"query": "list my Fabric tasks and mark the PR review one done",
"should_trigger": true
},
{
"query": "install the Fabric.so CLI and log in",
"should_trigger": true
},
{
"query": "run fabric ask over my project-atlas notes",
"should_trigger": true
},
{
"query": "generate zsh completion for the fabric command",
"should_trigger": true
},
{
"query": "use fab to list Microsoft Fabric lakehouses",
"should_trigger": false
},
{
"query": "create a Power BI semantic model using Microsoft Fabric CLI",
"should_trigger": false
},
{
"query": "what fabric should I use for a summer shirt?",
"should_trigger": false
},
{
"query": "search this local folder for meeting notes with ripgrep",
"should_trigger": false
},
{
"query": "write a Python script that uploads CSV rows into Postgres",
"should_trigger": false
},
{
"query": "summarise the attached PDF and do not save anything anywhere",
"should_trigger": false
}
][
{
"query": "my fabric command says auth failed when I try to search my library",
"should_trigger": true
},
{
"query": "create a Fabric note tagged agent-memory with the final decisions",
"should_trigger": true
},
{
"query": "browse Inbox/Reports in Fabric from the terminal",
"should_trigger": true
},
{
"query": "switch Fabric to the research workspace and upload this PDF",
"should_trigger": true
},
{
"query": "ask my Fabric assistant what I saved about project-atlas",
"should_trigger": true
},
{
"query": "check my Fabric subscription quotas from the CLI",
"should_trigger": true
},
{
"query": "Use the fabric --pattern summarize command from Daniel Miessler's project",
"should_trigger": false
},
{
"query": "Which fabric has the best drape for dressmaking?",
"should_trigger": false
},
{
"query": "Debug a Microsoft Fabric notebook that fails in OneLake",
"should_trigger": false
},
{
"query": "Make me a CLI cheat sheet for git",
"should_trigger": false
},
{
"query": "save this in Notion as a project memory",
"should_trigger": false
},
{
"query": "what does the word fabric mean in distributed systems?",
"should_trigger": false
}
]Using Fabric.so CLI as persistent agent memory
Fabric.so can act as a searchable memory layer for command-line agents because the CLI can search a Fabric library, save notes/files/links, and ask the Fabric assistant from the terminal. Use memory carefully: persistence is useful only when the saved information will help future sessions.
When to retrieve memory
Retrieve Fabric memory when the user explicitly says to use Fabric, Fabric memory, saved Fabric notes, or their Fabric library; project instructions say Fabric is the memory store; or the task depends on previous decisions, saved handoffs, meeting notes, research, or preferences likely stored in Fabric.
Avoid retrieval when the task is self-contained, unrelated to Fabric, or the user asks not to access external memory.
Retrieval pattern
1. Identify project/topic and likely tags. 2. Start with focused searches:
fabric --json search "PROJECT agent-memory" --tag agent-memory
fabric --json search "PROJECT decision" --tag decision-log
fabric --json search "PROJECT handoff" --tag handoff3. Broaden only if results are weak: fabric --json search "PROJECT". 4. Summarise useful findings with provenance. Discard irrelevant items. 5. If needed, ask Fabric's assistant: fabric --json ask "What are the active decisions and open questions for PROJECT?".
When to save memory
Save memory only when the user asks to save/remember/persist/create a handoff/use Fabric as memory; the workflow has established that Fabric memory should be updated; or the user has an ongoing project where saving a concise handoff is clearly expected.
Do not save memory just because a conversation happened. Persistence can become clutter or a privacy risk.
What to save
Good memory entries include decisions and rationale, current project state, unresolved questions, next actions and owners, important file paths/Fabric item titles/URLs/command outputs, and stable relevant constraints.
Avoid saving raw chat transcripts, secrets, tokens, passwords, private keys, cookies, credentials, sensitive personal details, speculative conclusions without uncertainty labels, and large command output when a concise summary would work.
Tagging scheme
Use stable tags so future agents can retrieve memory predictably: agent-memory, project-SHORTNAME, decision-log, handoff, todo, research, and meeting. Prefer lowercase, hyphenated tags. If installed help does not support multiple tags during creation, use agent-memory as the CLI tag and include the others in the note body.
Structured note format
Use assets/templates/agent-memory-note.md or generate one with:
python3 scripts/fabric_memory_note.py \
--title "PROJECT handoff" \
--project project-shortname \
--summary "Two to five sentences of future-useful context." \
--decision "Decision plus rationale." \
--open-question "Question still unresolved." \
--next-step "Concrete next step." \
--tag handoff \
--output /tmp/fabric-memory.mdReview the note before saving if it may contain sensitive material. The script redacts common token-like strings, but no automatic redaction is perfect.
Save using stdin:
fabric --json note --tag agent-memory < /tmp/fabric-memory.mdSession close checklist
Before saving at the end of a session, ask whether the note will help a future agent act better, is concise enough to retrieve quickly, is free of secrets and unnecessary personal data, includes source/provenance where useful, and has stable tags.
Fabric.so CLI command reference
This file is a compact, agent-oriented reference for the Fabric.so CLI executable named fabric. It is based on the public Fabric User Guide page for the CLI, checked on 2026-04-29, plus the Fabric CLI download page for agent-memory positioning. Always prefer live fabric --help and fabric help COMMAND for syntax that may have changed.
Usage and global options
Documented usage:
fabric [options] [command]Global options:
| Option | Meaning | Agent guidance |
|---|---|---|
-V, --version | Output version number | Use during local diagnostics. |
-h, --help | Display help for command | Use before unrecognised options. |
--json | Output JSON in non-interactive mode | Prefer for parsing, filtering, summarisation, and scripting. |
JSON output is documented as a global option, but individual command behaviour may vary by CLI version. If fabric --json COMMAND fails, inspect help and retry without JSON for diagnostics.
Installation
Official installer:
curl -fsSL https://fabric.so/cli/install.sh | shDo not run this automatically unless the user explicitly requested installation in the current environment. For a safer workflow:
tmp_script="$(mktemp)"
curl -fsSL https://fabric.so/cli/install.sh -o "$tmp_script"
sed -n '1,200p' "$tmp_script"
# run with user approval:
sh "$tmp_script"Authentication and account commands
| Command | Purpose | Notes |
|---|---|---|
fabric login | Login via browser | Preferred for humans in an interactive terminal. |
fabric auth API_KEY | Store API key | Use only when securely supplied. Never echo or save the key. |
fabric logout | Clear stored credentials | State-changing; confirm first unless explicitly requested. |
fabric subscription | Show workspace plan, usage, quotas | Read-only; useful for quota problems. |
Search and browse
| Command | Purpose | Typical usage |
|---|---|---|
fabric search [options] [query] | Search the current Fabric workspace | Semantic/hybrid lookup by query and/or tag. |
fabric path [options] [query] | Browse items and locations | No argument lists root spaces; path-like queries browse locations. |
fabric inbox [options] | List Inbox items | Read-only location listing. |
fabric bin [options] | List Bin items | Read-only location listing. |
Examples:
fabric --json search "meeting notes"
fabric --json search "design" --tag work
fabric --json search --tag reading,todo
fabric --json path
fabric --json path "My Space"
fabric --json path "Inbox/Reports"
fabric --json inbox
fabric --json binUse search when the user describes content semantically. Use path when the user describes a location, space, folder, Inbox path, or wants to browse.
Create and save content
The docs state that note, link, and file are shortcuts for corresponding create commands. The save command is a smart command that detects URL, file path, text, or stdin and creates the appropriate resource.
| Command | Purpose | Notes |
|---|---|---|
fabric create KIND | Explicit resource creation | KIND can be space, folder, note, link, or file. |
fabric note [content] | Create a note | Reads stdin if content is omitted. Good for long generated text. |
fabric link URL | Save a bookmark | Use for URLs when type should be explicit. |
fabric file PATH | Upload a local file | Check path existence first. |
fabric save [input] | Smart-save | URL becomes bookmark, file path becomes upload, text becomes note, stdin is accepted. |
fabric folder [name] | Create a folder | Inbox by default unless --parent, /, or path is used. |
Examples:
fabric --json create note "Meeting summary" --tag work
fabric --json create note "Q1 Review" --parent "Work/Projects"
fabric --json create folder "Projects" --tag client
fabric --json create folder "Archive" --parent "Work" --tag archive
fabric --json create folder "My Space" --parent /
fabric --json create link "https://example.com" --title "Article"
fabric --json create file "$HOME/Desktop/report.pdf"
fabric --json note "Quick reminder" --tag remember
fabric --json note "Ideas" --parent "Work/Projects"
fabric --json link "https://example.com" --title "Article" --tag reading
fabric --json link "https://example.com" --parent "Work/Reading"
fabric --json file "$HOME/Downloads/report.pdf" --title "Q4 Report"
fabric --json file "$HOME/Downloads/report.pdf" --parent "Work/Reports"
fabric --json save "https://example.com" --tag reading
fabric --json save "$HOME/Downloads/doc.pdf" --title "Report"
fabric --json save "Remember to call dentist" --tag personalFor multi-line notes, prefer stdin:
cat /tmp/generated-note.md | fabric --json note --tag agent-memoryIf both a title and long body are needed, inspect fabric note --help or fabric create note --help for title/body support in the installed version before assuming syntax. If multiple tags are needed, inspect help for whether repeated --tag or comma-separated tags are supported for creation commands.
Tasks
| Command | Purpose | Notes |
|---|---|---|
fabric task list | List tasks | Supports documented --todo and --done. |
fabric task add TITLE | Create task | Supports documented --due-date YYYY-MM-DD and --priority HIGH. |
fabric task done TASK_ID | Mark task completed | Requires task ID. List first if unknown. |
fabric task edit TASK_ID | Edit task | Example uses --title "New title". |
fabric task rm TASK_ID | Delete task | Destructive; confirm first. |
fabric task help | Display task help | Use for current syntax. |
Do not pass a task title to task done, task edit, or task rm unless installed help explicitly supports that. The public docs show task ID syntax.
Workspaces, assistant, and completion
fabric --json workspace list
fabric --json workspace current
fabric workspace select "My Team"
fabric --json ask "summarize everything tagged with project-atlas"
fabric --json ask "find the onboarding notes and list open decisions"
fabric completion bash
fabric completion zsh
fabric completion fishUse ask when the user wants Fabric's assistant to reason over Fabric context or perform assistant-capable actions. Use search when the agent needs raw result candidates to inspect, rank, cite, or use in a deterministic workflow.
Completion generation is read-only, but installation into shell startup files is a local file write and should be done only when the user asks for it.
Undocumented or partially documented areas
The public page says almost all Fabric capabilities are exposed via the CLI, but it does not list complete syntax for every option, deletion/move/tag/edit command, or item-reading behaviour. For those tasks:
1. Run fabric help COMMAND and inspect options. 2. Use only syntax shown by installed help. 3. If no CLI operation is exposed, suggest Fabric MCP, the Fabric web app, or the API rather than guessing.
JSON handling principles
- Do not assume stable field names beyond what the installed CLI returns.
- Save raw JSON to a temporary file for complex parsing, then use
jqor Python. - Keep command output compact; large library searches may need filtering or summarisation.
- If output is not JSON despite
--json, record the command and stderr and fall back to text parsing only when safe.
Security and consent guide for Fabric.so CLI workflows
Fabric CLI operations can read from and write to a remote workspace. Use the following consent boundaries and safety habits.
Operation categories
| Category | Examples | Default behaviour |
|---|---|---|
| Local read-only | command -v fabric, fabric --version, fabric --help | Safe to run when relevant. |
| Remote read-only | search, path, inbox, bin, workspace current, workspace list, task list, subscription | Safe to run when user context implies Fabric access. |
| Non-destructive remote write | note, link, file, save, create folder, task add, task done, task edit, workspace select | Run only when the user clearly asks for the change or it is necessary for the requested workflow. |
| Destructive or hard-to-reverse | task rm, logout, deletion, bulk edits, broad moves, any deletion command found in help | Ask for explicit confirmation immediately before running. |
| Installation/authentication | installer, login, auth API_KEY | Do not run without explicit request and safe handling of credentials. |
Installation
The official installer is a remote shell script. That is normal for this CLI, but it still deserves care.
tmp_script="$(mktemp)"
curl -fsSL https://fabric.so/cli/install.sh -o "$tmp_script"
sed -n '1,200p' "$tmp_script"
# After approval:
sh "$tmp_script"Never run installer commands hidden inside another script or without telling the user what will happen.
Authentication
Preferred for humans: fabric login.
Non-interactive environments may need fabric auth "$FABRIC_API_KEY".
Rules: do not ask the user to paste API keys into ordinary chat unless there is no safer route; prefer environment variables, secret stores, or interactive login; never echo a key; never save keys to Fabric memory; redact tokens in command output before summarising.
Shell quoting and command history
User-provided content can contain spaces, quotes, command substitutions, shell metacharacters, or newlines. Avoid interpolating it directly into shell commands. For long generated text, write to a temp file and pipe stdin:
cat /tmp/note.md | fabric --json note --tag agent-memoryFile uploads
Before uploading a local path, run test -f "$PATH". Reject or ask before uploading credential files such as .env, SSH keys, API-token files, browser profile directories, password databases, private key material, large logs that may contain secrets, or broad user-home globs.
Workspace selection
Changing workspace changes where subsequent commands operate. Before write operations: fabric --json workspace current. When switching, list if needed, select, then re-check current workspace and report it in the final answer.
Memory persistence privacy
Fabric memory is persistent and searchable. Do not save secrets, unrelated sensitive personal data, private user preferences unless the user asked for persistent memory, raw transcripts, or uncertainty-free claims when evidence is weak.
Sources and provenance
Last checked: 2026-04-29.
Primary Fabric.so sources
1. Fabric User Guide: https://user-guide.fabric.so/ai-tools/CLI-usage
Used for CLI purpose; availability; install command; top-level usage; global options; command list; task actions; note/link/file shortcuts; save behaviour; and examples for search, path, create, shortcuts, smart save, tasks, and workspaces.
2. Fabric CLI download/product page: https://fabric.so/download/cli
Used for CLI positioning for developers and AI agents, examples of saving thoughts/asking/searching, and persistent memory for command-line agents.
Agent Skills sources supplied with the request
The skill structure follows the Agent Skills format: SKILL.md with frontmatter and optional scripts/, references/, and assets/ directories. The package also uses the supplied guidance on progressive disclosure, concise descriptions, trigger evals, output-quality evals, script design, and avoiding a root README inside the skill directory.
Design notes
- Command syntax is limited to syntax present in the Fabric public docs or guarded with a live-help check.
- The package intentionally distinguishes Fabric.so
fabricfrom Microsoft Fabricfab, Daniel Miessler's Fabric framework, Python Fabric SSH, Fabric.js, and textile/fashion prompts because those are common near-miss trigger failures. - Helper scripts are self-contained Python 3 scripts with no external dependencies. They are designed for non-interactive agent use with
--help, structured JSON output, safe defaults, and bounded output sizes.
Update procedure
1. Re-read the Fabric User Guide CLI page. 2. Compare commands and examples with references/command-reference.md. 3. Run python3 scripts/validate_skill.py . from the skill root. 4. Re-run trigger evals against near misses. 5. Update metadata.version, this source file, and the critical-analysis note.
Fabric.so CLI troubleshooting
fabric: command not found
Likely causes: Fabric.so CLI is not installed, the install directory is not on PATH, the shell has not been restarted after installation, or the user installed Microsoft Fabric fab, which is a different CLI.
Checks:
command -v fabric || true
command -v fab || true
python3 scripts/fabric_check.py --jsonIf only fab exists, explain that this skill targets Fabric.so fabric, not Microsoft Fabric fab.
CLI exists but help/version fails
Run fabric --version and fabric --help. Check aliases/functions, executable permissions, platform compatibility, and whether the binary is actually Fabric.so CLI.
Authentication failure
Symptoms include unauthorised, forbidden, not logged in, token expired, or browser login errors. Use fabric login in an interactive terminal, or fabric auth "$FABRIC_API_KEY" only with a securely supplied key. Do not print or save API keys.
Browser login is impossible in a headless environment
Options: run fabric login on a machine with a browser if credentials sync appropriately, use fabric auth with a secure API-key route, or ask the user to complete setup locally and rerun read-only checks.
Wrong workspace
Run:
fabric --json workspace current
fabric --json workspace list
fabric workspace select "Workspace Name"
fabric --json workspace currentBefore writes, report the target workspace.
JSON output fails or is not parseable
Possible causes: --json placement changed, the command does not support JSON in the installed version, error messages are printed alongside JSON, or CLI version differs from docs. Retry without JSON, run command-specific help, parse text only if small and unambiguous, and do not assume a schema from memory.
Parent path not found
Check with fabric --json path "Parent" and fabric --json path "Parent/Child". Create the missing folder only if the user asked for it or it is necessary for the requested write. Otherwise ask where to place the item.
File upload fails
Check test -f "$PATH" && stat "$PATH". Possible causes include nonexistent path, quoting/tilde expansion, file size, permissions, or quota. Check fabric --json subscription for quota issues.
Task update fails
Common mistake: calling fabric task done "Review PR" with a title. Public docs show ID syntax. Correct pattern:
fabric --json task list --todo
# choose TASK_ID from results
fabric --json task done "$TASK_ID"If multiple tasks match, ask the user to disambiguate. Never invent a task ID.
Ambiguous Fabric request
If the user mentions Microsoft Fabric, Power BI, OneLake, lakehouse, KQL, tenant, capacity, pipeline, semantic model, or fab, do not use Fabric.so commands. If the user mentions Daniel Miessler's Fabric, patterns, YouTube summarisation, fabric --pattern, or yt, this is also not Fabric.so CLI.
Large output or noisy search results
Add tags or path filters, search for quoted project names or unique phrases, use fabric ask for synthesis over many items, or save raw output to a temp file and summarise only selected entries.
Command not listed in this skill
Use live help and avoid guessing. If unavailable, suggest Fabric MCP, the web app, or API.
V2 critical analysis and improvement notes
This note records the audit of version 1 and the design changes made for version 2.
What v1 did well
- It identified the right core command surface: installation, auth, search, path browsing, create/save shortcuts, tasks, workspaces,
ask, completions, subscription, and JSON output. - It included useful safety guidance around API keys, installer consent, workspace verification, and destructive operations.
- It bundled helper scripts for environment checking and structured agent-memory notes.
- It included trigger and functional evals, which made the package more testable than a simple
SKILL.md. - It used progressive disclosure by moving detailed material into
references/,assets/, andevals/.
Main v1 risks and gaps
1. Trigger boundaries needed sharper near-miss handling. V2 adds an explicit first decision for Microsoft Fabric fab, Power BI/OneLake terms, Daniel Miessler's Fabric framework, Python Fabric SSH, Fabric.js, and physical fabric. 2. Some operations were too optimistic about undocumented CLI behaviour. V2 emphasises live help and avoids undocumented assumptions about JSON fields, item body retrieval, moving, deletion, tagging, and editing. 3. Consent boundaries were present but could be more operational. V2 has a dedicated operation matrix distinguishing local read-only, remote read-only, non-destructive writes, destructive writes, installation, and authentication. 4. Agent memory guidance needed stronger privacy and retrieval discipline. V2 clarifies when to retrieve and when to save, what belongs in memory, and how to tag notes consistently. 5. Evals needed more near-misses and train/validation trigger splits. V2 expands negative trigger coverage and adds evals for ambiguous Fabric requests, task IDs, installer consent, and long-note stdin handling. 6. The package benefits from a self-validator. V2 adds scripts/validate_skill.py to catch frontmatter issues, broken references, JSON errors, missing scripts, and overly long descriptions. 7. Multi-tag creation syntax is not fully established in the public docs. V2 avoids relying on it in primary examples and tells agents to check installed help before using multiple creation tags.
V2 improvements applied
- Rewrote the frontmatter description to be more intent-focused and precise while staying below the 1024-character limit.
- Added a prominent "First decision" section to prevent over-triggering on adjacent Fabric meanings.
- Added
references/security-and-consent.mdfor explicit safety policy. - Added
references/workflows.mdfor repeatable multi-step workflows. - Added
references/agent-memory.mdwith retrieval/save policies and tagging conventions. - Added
scripts/fabric_help_cache.pyto inspect installed CLI help and adapt to version changes. - Strengthened
scripts/fabric_check.pyfor read-only diagnostics and wrong-CLI detection. - Kept and improved
scripts/fabric_memory_note.pyfor redaction, stdin/file support, and output structure. - Added
scripts/fabric_command_plan.pyfor shell-quoted dry-run planning. - Added
scripts/validate_skill.pyfor package validation. - Expanded templates for memory notes, session bootstrap, search summaries, and task plans.
- Split trigger evals into train and validation files.
Remaining limitations
- The skill cannot guarantee exact JSON field names because those are not documented in the public source page.
- Commands beyond the public docs still require live help from the installed CLI.
- Authentication and workspace access can only be fully tested in a user's configured environment.
- Automatic secret redaction is best-effort; agents should still inspect memory notes before saving.
Fabric.so CLI workflow playbook
This file provides repeatable workflows for agents using the Fabric.so CLI. Use it when the user asks for a multi-step outcome rather than a single command.
Workflow 1: diagnose setup and readiness
Use when the user says the CLI is broken, asks to install/login, or wants to know whether their terminal is ready.
1. Run local read-only checks:
python3 scripts/fabric_check.py --json2. If status is not_installed, do not install automatically unless the user explicitly requested installation. Explain the official installer and suggest reviewing it first.
3. If the CLI exists and the user wants live Fabric access, run deeper read-only checks:
python3 scripts/fabric_check.py --deep --json4. Classify the failure: missing binary/PATH, wrong tool, CLI help/version failure, authentication problem, workspace selection problem, network issue, or quota issue.
5. Provide the smallest safe next command, such as fabric login, fabric workspace current, fabric workspace list, or fabric --json subscription.
Workflow 2: search Fabric and answer from results
Use when the user wants to retrieve notes, files, links, decisions, meeting notes, saved research, or project memory.
1. Determine query terms, tags, and workspace. 2. Verify current workspace if workspace matters. 3. Search broadly first: fabric --json search "QUERY". 4. Narrow by tag or folder/path if helpful. 5. Summarise results with provenance: query, tags, workspace, item titles, paths, URLs if returned, and limitations. 6. If CLI results only include snippets/titles and the user asks for exact content, use fabric ask or another available Fabric connector if appropriate. Do not invent missing item bodies.
Workflow 3: save a note, URL, or file
Use when the user asks to save, capture, upload, bookmark, or persist content.
1. Identify the content type: URL (link or save), local path (file or save), text/memory (note or save), or ambiguous (save). 2. Check workspace before writes when relevant: fabric --json workspace current. 3. For files, check local existence first: test -f "$PATH". 4. Use safe command patterns:
fabric --json link "https://example.com" --title "Article" --tag reading
fabric --json file "$HOME/Downloads/report.pdf" --title "Q4 Report" --parent "Work/Reports"
fabric --json save "Remember to call dentist" --tag personal5. For long text, write to a temp file or pipe stdin:
fabric --json note --tag agent-memory < /tmp/note.md6. Verify if useful by searching for a unique phrase. Report workspace, title/path/tag, and any verification result.
Workflow 4: create folders or organise locations
Use when the user asks to create spaces/folders or place content under a path.
1. Browse parent path: fabric --json path "Work". 2. Create missing folder only when requested or necessary for the requested write. 3. Do not move or delete existing items unless explicitly requested and confirmed if destructive. 4. Verify with fabric path "Parent/Child".
Workflow 5: manage tasks
Creating a task:
fabric --json task add "Task title" --due-date 2026-04-20
fabric --json task add "Review PR" --priority HIGHCompleting or editing a task when ID is unknown:
1. List tasks: fabric --json task list --todo. 2. Match the candidate task by title, status, due date, workspace, or context. 3. If exactly one candidate is found, run fabric --json task done "$TASK_ID". 4. If multiple candidates match, ask which one. Do not invent IDs. 5. For deletion, ask for explicit confirmation before fabric --json task rm "$TASK_ID".
Workflow 6: temporary workspace switch
1. Record current workspace: fabric --json workspace current. 2. List workspaces if the name is not exact: fabric --json workspace list. 3. Select: fabric workspace select "My Team". 4. Re-check: fabric --json workspace current. 5. Perform the requested operation. 6. If the user asked for a temporary switch, restore the original workspace and report both changes.
Workflow 7: use Fabric as agent memory
At task start:
fabric --json search "project-atlas agent-memory" --tag agent-memory
fabric --json search "project-atlas decision" --tag decision-logDuring the task, keep memory candidates concise. Do not dump many irrelevant results into context.
At task end, only if opted in, generate and review a structured memory note:
python3 scripts/fabric_memory_note.py \
--title "Project Atlas session handoff" \
--project project-atlas \
--summary-file /tmp/session-summary.txt \
--decision "Use high-recall index for nightly retrieval." \
--next-step "Benchmark precision next week." \
--tag handoff \
--output /tmp/fabric-memory.md
fabric --json note --tag agent-memory < /tmp/fabric-memory.mdSave only future-useful information, not raw transcripts. If installed help supports multiple tags for creation, add project and category tags.
Workflow 8: shell completion
Generate completion scripts on request:
fabric completion bash
fabric completion zsh
fabric completion fishIf the user wants installation into their shell configuration, treat that as a local file modification. Inspect the target file path, append carefully, and preserve a backup.
Workflow 9: ask Fabric assistant vs search
Use search when the agent needs candidates, paths, IDs, result metadata, deterministic provenance, parsing, filtering, or choosing items.
Use ask when the user wants a natural-language answer over Fabric context, asks Fabric's assistant to summarise/find/act, or CLI search cannot expose enough item content.
#!/usr/bin/env python3
"""Read-only diagnostics for the Fabric.so CLI.
The script checks whether the Fabric.so CLI executable named `fabric` is on PATH,
optionally runs read-only account/workspace checks, and emits concise structured
output for agents. It never installs software, authenticates, logs out, changes
workspace, writes to Fabric, or deletes anything.
"""
from __future__ import annotations
import argparse
import json
import os
import re
import shutil
import subprocess
import sys
import time
from dataclasses import asdict, dataclass, field
from typing import Iterable, Sequence
SECRET_PATTERNS = [
re.compile(r"(?i)(api[_-]?key|token|secret|password)\s*[:=]\s*[^\s,;]+"),
re.compile(r"(?i)bearer\s+[a-z0-9._~+/=-]{12,}"),
re.compile(r"\bsk-[A-Za-z0-9_-]{12,}\b"),
re.compile(r"\b[a-f0-9]{32,}\b", re.IGNORECASE),
]
@dataclass
class CommandResult:
name: str
command: list[str]
ok: bool
returncode: int | None
duration_ms: int
stdout_preview: str = ""
stderr_preview: str = ""
error: str = ""
@dataclass
class DiagnosticReport:
status: str
installed: bool
fabric_path: str | None
fab_path: str | None
summary: list[str] = field(default_factory=list)
checks: list[CommandResult] = field(default_factory=list)
recommendations: list[str] = field(default_factory=list)
def redact(text: str) -> str:
value = text or ""
for pattern in SECRET_PATTERNS:
value = pattern.sub("[REDACTED_SECRET]", value)
return value
def preview(text: str, limit: int) -> str:
cleaned = redact(text).replace("\r", "")
if len(cleaned) <= limit:
return cleaned
return cleaned[:limit] + f"\n...[truncated {len(cleaned) - limit} chars]"
def run_command(name: str, command: Sequence[str], timeout: float, max_output: int) -> CommandResult:
started = time.monotonic()
try:
proc = subprocess.run(list(command), text=True, capture_output=True, timeout=timeout, check=False)
elapsed = int((time.monotonic() - started) * 1000)
return CommandResult(
name=name,
command=list(command),
ok=proc.returncode == 0,
returncode=proc.returncode,
duration_ms=elapsed,
stdout_preview=preview(proc.stdout, max_output),
stderr_preview=preview(proc.stderr, max_output),
)
except subprocess.TimeoutExpired as exc:
elapsed = int((time.monotonic() - started) * 1000)
return CommandResult(
name=name,
command=list(command),
ok=False,
returncode=None,
duration_ms=elapsed,
stdout_preview=preview(exc.stdout or "", max_output) if isinstance(exc.stdout, str) else "",
stderr_preview=preview(exc.stderr or "", max_output) if isinstance(exc.stderr, str) else "",
error=f"Timed out after {timeout:g}s",
)
except OSError as exc:
elapsed = int((time.monotonic() - started) * 1000)
return CommandResult(name=name, command=list(command), ok=False, returncode=None, duration_ms=elapsed, error=str(exc))
def build_report(deep: bool, timeout: float, max_output: int) -> DiagnosticReport:
fabric_path = shutil.which("fabric")
fab_path = shutil.which("fab")
report = DiagnosticReport(status="unknown", installed=bool(fabric_path), fabric_path=fabric_path, fab_path=fab_path)
if not fabric_path:
report.status = "not_installed"
report.summary.append("No `fabric` executable was found on PATH.")
if fab_path:
report.summary.append("A `fab` executable was found. That is commonly Microsoft Fabric CLI, not Fabric.so CLI.")
report.recommendations.append("Use the Fabric.so CLI executable named `fabric`; do not substitute `fab`.")
report.recommendations.append("Install only with user approval: curl -fsSL https://fabric.so/cli/install.sh | sh")
report.recommendations.append("After installation, restart the shell or update PATH, then rerun this checker.")
return report
report.summary.append(f"Found Fabric.so CLI candidate at: {fabric_path}")
if fab_path:
report.summary.append(f"Also found `fab` at: {fab_path}. Keep Fabric.so `fabric` distinct from Microsoft Fabric `fab`.")
local_commands = [
("version", ["fabric", "--version"]),
("top_level_help", ["fabric", "--help"]),
]
for name, cmd in local_commands:
report.checks.append(run_command(name, cmd, timeout, max_output))
if any(not c.ok for c in report.checks):
report.status = "local_error"
report.summary.append("The `fabric` executable exists but one or more local checks failed.")
report.recommendations.append("Run `fabric --help` manually and check aliases, permissions, and whether this is the Fabric.so binary.")
return report
if not deep:
report.status = "installed_local_ok"
report.summary.append("Local CLI checks passed. Run with --deep for read-only account and workspace checks.")
report.recommendations.append("For live Fabric workflows, verify auth/workspace with `python3 scripts/fabric_check.py --deep --json`.")
return report
remote_commands = [
("workspace_current_json", ["fabric", "--json", "workspace", "current"]),
("workspace_list_json", ["fabric", "--json", "workspace", "list"]),
("task_list_json", ["fabric", "--json", "task", "list"]),
("subscription_json", ["fabric", "--json", "subscription"]),
]
start = len(report.checks)
for name, cmd in remote_commands:
report.checks.append(run_command(name, cmd, timeout, max_output))
if any(not c.ok for c in report.checks[start:]):
report.status = "needs_auth_or_workspace"
report.summary.append("Local CLI checks passed, but at least one read-only account/workspace check failed.")
report.recommendations.append("Use `fabric login` in an interactive terminal, or `fabric auth` with a securely supplied API key for headless setup.")
report.recommendations.append("Run `fabric workspace current` and `fabric workspace list` after authentication.")
report.recommendations.append("If JSON output failed, retry the same command without `--json` for diagnostics.")
else:
report.status = "ok"
report.summary.append("Local and read-only account/workspace checks passed.")
return report
def parse_args(argv: Iterable[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Read-only diagnostic checks for the Fabric.so CLI executable named fabric.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("--deep", action="store_true", help="Run read-only account/workspace checks that contact Fabric.")
parser.add_argument("--json", action="store_true", help="Emit a JSON diagnostic report.")
parser.add_argument("--timeout", type=float, default=15.0, help="Timeout per command in seconds.")
parser.add_argument("--max-output", type=int, default=2000, help="Maximum stdout/stderr preview characters per command.")
parser.add_argument("--strict", action="store_true", help="Return non-zero for any non-ok status.")
return parser.parse_args(list(argv))
def print_text(report: DiagnosticReport) -> None:
print(f"Fabric CLI diagnostic status: {report.status}")
print(f"Installed: {report.installed}")
if report.fabric_path:
print(f"fabric path: {report.fabric_path}")
if report.fab_path:
print(f"fab path: {report.fab_path}")
print("\nSummary:")
for item in report.summary:
print(f"- {item}")
if report.recommendations:
print("\nRecommendations:")
for item in report.recommendations:
print(f"- {item}")
def main(argv: Iterable[str] = sys.argv[1:]) -> int:
args = parse_args(argv)
report = build_report(args.deep, args.timeout, args.max_output)
if args.json:
print(json.dumps(asdict(report), indent=2, sort_keys=True))
else:
print_text(report)
if args.strict and report.status != "ok":
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""Build a shell-quoted Fabric CLI command plan without executing it."""
from __future__ import annotations
import argparse
import json
import shlex
import sys
from pathlib import Path
from typing import Any
READ_ACTIONS = {"search", "path", "workspace-current", "workspace-list", "task-list", "inbox", "bin", "subscription", "ask-read"}
WRITE_ACTIONS = {"note", "link", "file", "save", "folder", "task-add", "task-done", "task-edit", "workspace-select", "ask-write"}
DESTRUCTIVE_ACTIONS = {"task-rm"}
def q(value: str) -> str:
return shlex.quote(value)
def add_tags(parts: list[str], tags: list[str] | None) -> None:
for tag in tags or []:
parts.extend(["--tag", q(tag)])
def add_optional(parts: list[str], flag: str, value: str | None) -> None:
if value:
parts.extend([flag, q(value)])
def risk_for(action: str) -> str:
if action in READ_ACTIONS:
return "read"
if action in DESTRUCTIVE_ACTIONS:
return "destructive"
return "write"
def command_plan(args: argparse.Namespace) -> dict[str, Any]:
action = args.action
parts: list[str]
verification: list[str] = []
notes: list[str] = []
if action == "search":
if not args.query:
raise ValueError("--query is required for search")
parts = ["fabric"]
if args.prefer_json:
parts.append("--json")
parts.extend(["search", q(args.query)])
add_tags(parts, args.tag)
verification.append("Review returned results and summarise only relevant matches.")
elif action == "path":
parts = ["fabric", "path"]
if args.query:
parts.append(q(args.query))
verification.append("Confirm the expected space or folder appears in the output.")
elif action == "note":
if args.content_file:
parts = ["cat", q(args.content_file), "|", "fabric", "note"]
notes.append("Inspect and redact the local file before executing.")
elif args.text:
parts = ["printf", "%s\\n", q(args.text), "|", "fabric", "note"]
else:
raise ValueError("Use --text or --content-file for note")
add_optional(parts, "--parent", args.parent)
add_tags(parts, args.tag)
verification.append("Search the exact first heading or browse the parent path to confirm creation if CLI output is not enough.")
elif action == "link":
if not args.url:
raise ValueError("--url is required for link")
parts = ["fabric", "link", q(args.url)]
add_optional(parts, "--title", args.title)
add_optional(parts, "--parent", args.parent)
add_tags(parts, args.tag)
verification.append("Search exact URL or title if the CLI output lacks an identifier.")
elif action == "file":
if not args.path:
raise ValueError("--path is required for file")
parts = ["fabric", "file", q(args.path)]
add_optional(parts, "--title", args.title)
add_optional(parts, "--parent", args.parent)
add_tags(parts, args.tag)
verification.extend(["Before executing, verify the local file exists and is readable.", "Browse the parent path or search the exact title after upload."])
elif action == "save":
if args.content_file:
parts = ["cat", q(args.content_file), "|", "fabric", "save"]
elif args.text:
parts = ["fabric", "save", q(args.text)]
elif args.url:
parts = ["fabric", "save", q(args.url)]
elif args.path:
parts = ["fabric", "save", q(args.path)]
else:
raise ValueError("Use --text, --url, --path, or --content-file for save")
add_optional(parts, "--title", args.title)
add_optional(parts, "--parent", args.parent)
add_tags(parts, args.tag)
verification.append("Verify inferred type and created item from CLI output or follow-up search.")
elif action == "folder":
if not args.title:
raise ValueError("--title is required for folder name")
parts = ["fabric", "folder", q(args.title)]
add_optional(parts, "--parent", args.parent)
add_tags(parts, args.tag)
verification.append("Use `fabric path` to confirm the folder exists.")
elif action == "task-list":
parts = ["fabric", "task", "list"]
if args.done:
parts.append("--done")
elif args.todo:
parts.append("--todo")
verification.append("Use returned task IDs for later task operations.")
elif action == "task-add":
if not args.title:
raise ValueError("--title is required for task-add")
parts = ["fabric", "task", "add", q(args.title)]
add_optional(parts, "--due-date", args.due_date)
add_optional(parts, "--priority", args.priority)
verification.append("List tasks to confirm the new task if CLI output is ambiguous.")
elif action in {"task-done", "task-edit", "task-rm"}:
if not args.task_id:
raise ValueError(f"--task-id is required for {action}")
sub = action.split("-")[1]
parts = ["fabric", "task", sub, q(args.task_id)]
if action == "task-edit":
add_optional(parts, "--title", args.title)
verification.append("Run `fabric task list` before this command to verify task ID and after it to verify result.")
elif action == "workspace-current":
parts = ["fabric", "workspace", "current"]
verification.append("Use this workspace name in the final response.")
elif action == "workspace-list":
parts = ["fabric", "workspace", "list"]
verification.append("Use exact workspace name for selection.")
elif action == "workspace-select":
if not args.workspace:
raise ValueError("--workspace is required for workspace-select")
parts = ["fabric", "workspace", "select", q(args.workspace)]
verification.append("Run `fabric workspace current` after selection and report active workspace.")
elif action in {"ask-read", "ask-write"}:
if not args.query:
raise ValueError("--query is required for ask")
parts = ["fabric", "ask", q(args.query)]
verification.append("For write-like ask requests, verify the action with search, path, or task list before reporting success.")
elif action in {"inbox", "bin", "subscription"}:
parts = ["fabric", action]
verification.append("Summarise relevant output; do not dump large raw lists.")
else:
raise ValueError(f"Unsupported action: {action}")
risk = risk_for(action)
return {"action": action, "risk": risk, "requires_confirmation": risk == "destructive" or action == "workspace-select", "commands": [" ".join(parts)], "verification": verification, "notes": notes}
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Build a shell-quoted Fabric CLI command plan without executing it.", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--action", required=True, choices=sorted(READ_ACTIONS | WRITE_ACTIONS | DESTRUCTIVE_ACTIONS), help="Intent/action to plan.")
parser.add_argument("--query", help="Search query, path query, or ask question depending on action.")
parser.add_argument("--text", help="Short note/save text. Prefer --content-file for long content.")
parser.add_argument("--content-file", help="Local UTF-8 file to pipe into note/save.")
parser.add_argument("--url", help="URL for link/save.")
parser.add_argument("--path", help="Local file path for file/save.")
parser.add_argument("--title", help="Title, folder name, task title, or file/link title depending on action.")
parser.add_argument("--parent", help="Fabric parent path.")
parser.add_argument("--tag", action="append", help="Tag to add. May be repeated.")
parser.add_argument("--task-id", help="Task ID for task-done/edit/rm.")
parser.add_argument("--due-date", help="Task due date, ideally YYYY-MM-DD.")
parser.add_argument("--priority", help="Task priority such as HIGH if supported by installed CLI.")
parser.add_argument("--workspace", help="Workspace name for workspace-select.")
parser.add_argument("--todo", action="store_true", help="For task-list, list pending tasks.")
parser.add_argument("--done", action="store_true", help="For task-list, list completed tasks.")
parser.add_argument("--prefer-json", action="store_true", help="For search, include global --json in the planned command.")
parser.add_argument("--shell-only", action="store_true", help="Print only the planned shell command instead of JSON.")
return parser.parse_args(argv)
def main(argv: list[str] = sys.argv[1:]) -> int:
args = parse_args(argv)
if args.content_file and not Path(args.content_file).exists():
print(f"Error: --content-file does not exist: {args.content_file}", file=sys.stderr)
return 2
try:
plan = command_plan(args)
except ValueError as exc:
print(f"Error: {exc}", file=sys.stderr)
return 2
print(plan["commands"][0] if args.shell_only else json.dumps(plan, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""Capture read-only help output from the installed Fabric.so CLI.
Agents can run this before using commands whose options may have changed. The
script emits bounded, redacted JSON or Markdown. It never performs Fabric writes.
"""
from __future__ import annotations
import argparse
import json
import re
import shutil
import subprocess
import sys
import time
from dataclasses import asdict, dataclass
from typing import Iterable, Sequence
SECRET_PATTERNS = [
re.compile(r"(?i)(api[_-]?key|token|secret|password)\s*[:=]\s*[^\s,;]+"),
re.compile(r"(?i)bearer\s+[a-z0-9._~+/=-]{12,}"),
re.compile(r"\bsk-[A-Za-z0-9_-]{12,}\b"),
]
DEFAULT_COMMANDS = ["search", "path", "create", "note", "link", "file", "save", "folder", "workspace", "ask", "task", "completion", "subscription"]
@dataclass
class HelpResult:
command_name: str
attempted_commands: list[list[str]]
ok: bool
returncode: int | None
duration_ms: int
output: str
stderr: str
def redact(text: str) -> str:
value = text or ""
for pattern in SECRET_PATTERNS:
value = pattern.sub("[REDACTED_SECRET]", value)
return value
def truncate(text: str, limit: int) -> str:
value = redact(text).replace("\r", "")
if len(value) <= limit:
return value
return value[:limit] + f"\n...[truncated {len(value) - limit} chars]"
def run(cmd: Sequence[str], timeout: float) -> subprocess.CompletedProcess[str] | None:
try:
return subprocess.run(list(cmd), text=True, capture_output=True, timeout=timeout, check=False)
except (subprocess.TimeoutExpired, OSError):
return None
def capture_help(command_name: str, timeout: float, max_output: int) -> HelpResult:
attempts: list[list[str]] = []
started = time.monotonic()
if command_name in {"", "root", "--help"}:
candidates = [["fabric", "--help"]]
label = "root"
else:
label = command_name
candidates = [["fabric", "help", command_name], ["fabric", command_name, "--help"]]
last_returncode: int | None = None
last_stdout = ""
last_stderr = ""
for candidate in candidates:
attempts.append(candidate)
proc = run(candidate, timeout)
if proc is None:
last_returncode = None
last_stderr = f"Command failed or timed out: {' '.join(candidate)}"
continue
last_returncode = proc.returncode
last_stdout = proc.stdout
last_stderr = proc.stderr
if proc.returncode == 0 and (proc.stdout.strip() or proc.stderr.strip()):
elapsed = int((time.monotonic() - started) * 1000)
return HelpResult(label, attempts, True, proc.returncode, elapsed, truncate(proc.stdout or proc.stderr, max_output), truncate(proc.stderr, max_output))
elapsed = int((time.monotonic() - started) * 1000)
return HelpResult(label, attempts, False, last_returncode, elapsed, truncate(last_stdout, max_output), truncate(last_stderr, max_output))
def parse_commands(value: str | None) -> list[str]:
if not value:
return ["root", *DEFAULT_COMMANDS]
result = [p.strip() for p in value.split(",") if p.strip()]
return result or ["root", *DEFAULT_COMMANDS]
def parse_args(argv: Iterable[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Capture help output from the installed Fabric.so CLI.", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--commands", help="Comma-separated command names, e.g. search,save,task. Omit for common commands.")
parser.add_argument("--json", action="store_true", help="Emit JSON instead of Markdown.")
parser.add_argument("--timeout", type=float, default=10.0, help="Timeout per help command in seconds.")
parser.add_argument("--max-output", type=int, default=6000, help="Maximum characters per captured help output.")
parser.add_argument("--strict", action="store_true", help="Return non-zero if any requested help command fails.")
return parser.parse_args(list(argv))
def main(argv: Iterable[str] = sys.argv[1:]) -> int:
args = parse_args(argv)
if not shutil.which("fabric"):
payload = {"status": "not_installed", "message": "No `fabric` executable found on PATH."}
print(json.dumps(payload, indent=2, sort_keys=True) if args.json else payload["message"])
return 2 if args.strict else 0
results = [capture_help(name, args.timeout, args.max_output) for name in parse_commands(args.commands)]
if args.json:
print(json.dumps({"status": "ok" if all(r.ok for r in results) else "partial", "results": [asdict(r) for r in results]}, indent=2, sort_keys=True))
else:
for result in results:
print(f"## {result.command_name} ({'ok' if result.ok else 'failed'})\n")
for cmd in result.attempted_commands:
print(f"- `{' '.join(cmd)}`")
if result.output:
print("\n```text")
print(result.output.rstrip())
print("```\n")
if args.strict and not all(r.ok for r in results):
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""Generate a structured, redacted Fabric memory note.
The script prints Markdown by default, or JSON with --json. It does not call the
Fabric CLI. Agents can inspect the output, then pipe it to `fabric note` or
`fabric save` when the user has opted into persistence.
"""
from __future__ import annotations
import argparse
import datetime as dt
import json
import re
import sys
from pathlib import Path
SECRET_PATTERNS = [
re.compile(r"(?i)(api[_-]?key|token|secret|password|passwd|pwd)\s*[:=]\s*[^\s,;]+"),
re.compile(r"(?i)bearer\s+[a-z0-9._~+/=-]{12,}"),
re.compile(r"\bsk-[A-Za-z0-9_-]{12,}\b"),
re.compile(r"\bgh[pousr]_[A-Za-z0-9_]{20,}\b"),
re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----", re.DOTALL),
re.compile(r"\b[a-f0-9]{32,}\b", re.IGNORECASE),
]
def redact(text: str) -> str:
value = text or ""
for pattern in SECRET_PATTERNS:
value = pattern.sub("[REDACTED_SECRET]", value)
return value
def read_text(value: str | None, file_value: str | None, stdin: bool) -> str:
if file_value:
return Path(file_value).read_text(encoding="utf-8")
if stdin:
return sys.stdin.read()
return value or ""
def normalise_items(values: list[str] | None) -> list[str]:
items: list[str] = []
for value in values or []:
for line in str(value).splitlines():
cleaned = redact(line.strip())
if cleaned:
items.append(cleaned)
return items
def normalise_tag(tag: str) -> str:
tag = redact(tag.strip().lower().replace(" ", "-"))
tag = re.sub(r"[^a-z0-9._-]+", "-", tag)
return re.sub(r"-+", "-", tag).strip("-")
def normalise_tags(values: list[str] | None, project: str) -> list[str]:
tags = ["agent-memory"]
if project:
tags.append(normalise_tag(project))
for value in values or []:
for part in str(value).split(","):
tag = normalise_tag(part)
if tag:
tags.append(tag)
deduped: list[str] = []
for tag in tags:
if tag and tag not in deduped:
deduped.append(tag)
return deduped
def bullets(items: list[str]) -> str:
return "\n".join(f"- {item}" for item in items) if items else "- None recorded."
def clamp(text: str, max_chars: int) -> tuple[str, bool]:
if max_chars <= 0 or len(text) <= max_chars:
return text, False
return text[:max_chars].rstrip() + f"\n\n[Truncated to {max_chars} characters before saving.]", True
def build_payload(args: argparse.Namespace) -> dict[str, object]:
title = redact(args.title.strip())
project = redact(args.project.strip()) if args.project else "Not specified"
raw_summary = read_text(args.summary, args.summary_file, args.stdin_summary).strip()
summary, truncated = clamp(redact(raw_summary), args.max_summary_chars)
tags = normalise_tags(args.tag, args.project)
date = args.date or dt.date.today().isoformat()
note = "\n".join([
f"# {title}", "",
f"Date: {date}",
f"Project: {project}",
f"Tags: {', '.join(tags)}", "",
"## Summary", summary or "No summary provided.", "",
"## Decisions", bullets(normalise_items(args.decision)), "",
"## Evidence or context", bullets(normalise_items(args.evidence)), "",
"## Open questions", bullets(normalise_items(args.open_question)), "",
"## Next steps", bullets(normalise_items(args.next_step)), "",
"## Safety notes",
"- Secrets and token-like strings were redacted automatically, but review before saving to Fabric.",
"- The summary was truncated before output." if truncated else "",
]).rstrip() + "\n"
return {"title": title, "project": project, "tags": tags, "note": note, "summary_truncated": truncated}
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Generate a structured, redacted Fabric memory note without calling Fabric.", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--title", default="Fabric agent memory", help="Markdown note title.")
parser.add_argument("--project", default="", help="Project/topic identifier, e.g. project-atlas.")
parser.add_argument("--date", default=dt.date.today().isoformat(), help="Date to include in the note.")
parser.add_argument("--summary", help="Summary text. Use --summary-file or --stdin-summary for longer content.")
parser.add_argument("--summary-file", help="Read UTF-8 summary text from this file.")
parser.add_argument("--stdin-summary", action="store_true", help="Read summary text from stdin.")
parser.add_argument("--decision", action="append", help="Decision bullet. May be repeated.")
parser.add_argument("--evidence", action="append", help="Evidence/context bullet. May be repeated.")
parser.add_argument("--open-question", action="append", help="Open question bullet. May be repeated.")
parser.add_argument("--next-step", action="append", help="Next-step bullet. May be repeated.")
parser.add_argument("--tag", action="append", help="Additional tag. May be repeated or comma-separated.")
parser.add_argument("--max-summary-chars", type=int, default=6000, help="Maximum summary characters before truncation. Use 0 for no limit.")
parser.add_argument("--output", help="Write output to this file instead of stdout.")
parser.add_argument("--json", action="store_true", help="Print JSON containing title, project, tags, note, and truncation status.")
return parser.parse_args(argv)
def main(argv: list[str] = sys.argv[1:]) -> int:
args = parse_args(argv)
payload = build_payload(args)
output = json.dumps(payload, indent=2, sort_keys=True) + "\n" if args.json else str(payload["note"])
if args.output:
Path(args.output).write_text(output, encoding="utf-8")
print(f"Wrote {args.output}", file=sys.stderr)
else:
print(output, end="")
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""Validate the fabric-cli skill package structure.
This authoring helper checks common Agent Skill packaging issues without any
external dependencies. It is intentionally conservative and does not replace an
official skills-ref validator.
"""
from __future__ import annotations
import argparse
import json
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import Iterable
FRONTMATTER_RE = re.compile(r"\A---\n(.*?)\n---\n", re.DOTALL)
LINK_RE = re.compile(r"\[[^\]]+\]\(([^)]+)\)")
LOCAL_PATH_RE = re.compile(r"(?<![\w/.-])((?:references|scripts|assets|evals)/[A-Za-z0-9_./-]+)")
NAME_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
def parse_simple_yaml(frontmatter: str) -> dict[str, object]:
data: dict[str, object] = {}
current_map: dict[str, str] | None = None
for raw in frontmatter.splitlines():
if not raw.strip():
continue
if raw.startswith(" ") and current_map is not None:
if ":" not in raw:
raise ValueError(f"Invalid nested frontmatter line: {raw!r}")
key, value = raw.strip().split(":", 1)
current_map[key.strip()] = value.strip().strip('"')
continue
current_map = None
if ":" not in raw:
raise ValueError(f"Invalid frontmatter line: {raw!r}")
key, value = raw.split(":", 1)
key = key.strip()
value = value.strip()
if value == "":
current_map = {}
data[key] = current_map
else:
data[key] = value.strip('"')
return data
def add(result: list[dict[str, object]], ok: bool, check: str, detail: str = "") -> None:
result.append({"ok": ok, "check": check, "detail": detail})
def validate(root: Path, run_script_help: bool) -> list[dict[str, object]]:
results: list[dict[str, object]] = []
skill_md = root / "SKILL.md"
add(results, skill_md.exists(), "SKILL.md exists", str(skill_md))
if not skill_md.exists():
return results
text = skill_md.read_text(encoding="utf-8")
match = FRONTMATTER_RE.match(text)
add(results, bool(match), "SKILL.md has YAML frontmatter delimiters")
if not match:
return results
try:
fm = parse_simple_yaml(match.group(1))
add(results, True, "frontmatter parses with simple validator")
except Exception as exc:
add(results, False, "frontmatter parses with simple validator", str(exc))
return results
name = str(fm.get("name", ""))
desc = str(fm.get("description", ""))
compat = str(fm.get("compatibility", "")) if "compatibility" in fm else ""
add(results, bool(name), "name present")
add(results, bool(NAME_RE.fullmatch(name)), "name is kebab-case lowercase alphanumeric", name)
add(results, name == root.name, "name matches parent directory", f"name={name!r}, dir={root.name!r}")
add(results, 1 <= len(desc) <= 1024, "description length is 1-1024 chars", f"length={len(desc)}")
add(results, "Use this skill" in desc or "Use" in desc, "description has use/trigger phrasing")
add(results, len(compat) <= 500, "compatibility length is at most 500 chars", f"length={len(compat)}")
add(results, "<" not in match.group(1) and ">" not in match.group(1), "frontmatter contains no angle brackets")
add(results, not (root / "README.md").exists(), "no root README.md inside skill folder")
for path in sorted(root.glob("evals/*.json")):
try:
json.loads(path.read_text(encoding="utf-8"))
add(results, True, f"valid JSON: {path.relative_to(root)}")
except Exception as exc:
add(results, False, f"valid JSON: {path.relative_to(root)}", str(exc))
docs = [skill_md] + sorted((root / "references").glob("*.md"))
seen_paths: set[str] = set()
for doc in docs:
content = doc.read_text(encoding="utf-8")
for link in LINK_RE.findall(content):
if re.match(r"https?://|mailto:", link):
continue
seen_paths.add(link.split("#", 1)[0])
for local in LOCAL_PATH_RE.findall(content):
seen_paths.add(local)
for rel in sorted(seen_paths):
if not rel or rel.startswith("/"):
continue
add(results, (root / rel).exists(), f"referenced local path exists: {rel}")
for script in sorted((root / "scripts").glob("*.py")):
add(results, os.access(script, os.X_OK), f"script executable bit set: {script.relative_to(root)}")
if run_script_help:
try:
proc = subprocess.run([sys.executable, str(script), "--help"], capture_output=True, text=True, timeout=8, check=False)
add(results, proc.returncode == 0 and "usage" in proc.stdout.lower(), f"script --help works: {script.relative_to(root)}")
except Exception as exc:
add(results, False, f"script --help works: {script.relative_to(root)}", str(exc))
return results
def parse_args(argv: Iterable[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Validate a Fabric CLI Agent Skill package.")
parser.add_argument("root", nargs="?", default=".", help="Skill root directory.")
parser.add_argument("--json", action="store_true", help="Emit JSON result.")
parser.add_argument("--run-script-help", action="store_true", help="Run each script with --help.")
return parser.parse_args(list(argv))
def main(argv: Iterable[str] = sys.argv[1:]) -> int:
args = parse_args(argv)
root = Path(args.root).resolve()
results = validate(root, args.run_script_help)
ok = all(item["ok"] for item in results)
if args.json:
print(json.dumps({"ok": ok, "results": results}, indent=2, sort_keys=True))
else:
for item in results:
mark = "PASS" if item["ok"] else "FAIL"
detail = f" — {item['detail']}" if item.get("detail") else ""
print(f"{mark}: {item['check']}{detail}")
return 0 if ok else 2
if __name__ == "__main__":
raise SystemExit(main())