@specset/cli 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specset/cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Command-line interface for Specset — authenticate, run GraphQL queries, and chat with Specset project agents.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://specset.com",
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "specset",
3
+ "version": "0.6.0",
4
+ "description": "Search and manage Specset construction project data with shared skills and MCP tools.",
5
+ "author": {
6
+ "name": "Specset"
7
+ },
8
+ "homepage": "https://docs.specset.com",
9
+ "repository": "https://github.com/specsetai/specset-plugins",
10
+ "license": "Apache-2.0",
11
+ "keywords": [
12
+ "construction",
13
+ "drawings",
14
+ "specifications",
15
+ "submittals",
16
+ "rfis"
17
+ ]
18
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "specset",
3
+ "version": "0.6.0",
4
+ "description": "Search and manage Specset construction project data with shared skills and MCP tools.",
5
+ "author": {
6
+ "name": "Specset",
7
+ "email": "support@specset.com",
8
+ "url": "https://specset.com"
9
+ },
10
+ "homepage": "https://docs.specset.com",
11
+ "repository": "https://github.com/specsetai/specset-plugins",
12
+ "license": "Apache-2.0",
13
+ "keywords": [
14
+ "construction",
15
+ "drawings",
16
+ "specifications",
17
+ "submittals",
18
+ "rfis"
19
+ ],
20
+ "mcpServers": "./.mcp.json",
21
+ "interface": {
22
+ "displayName": "Specset",
23
+ "shortDescription": "Search and manage construction project data",
24
+ "longDescription": "Use Specset skills and MCP tools to search specifications and drawings, inspect project records, manage workflows, and transfer files.",
25
+ "developerName": "Specset",
26
+ "category": "Productivity",
27
+ "capabilities": [
28
+ "Skills",
29
+ "MCP",
30
+ "File transfer"
31
+ ],
32
+ "websiteURL": "https://specset.com",
33
+ "privacyPolicyURL": "https://specset.com/privacy",
34
+ "termsOfServiceURL": "https://specset.com/terms",
35
+ "defaultPrompt": [
36
+ "Find the project requirements for this scope.",
37
+ "Compare this submittal with the specifications.",
38
+ "Show me the latest RFIs affecting this work."
39
+ ],
40
+ "brandColor": "#1F6FEB"
41
+ }
42
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "specset": {
4
+ "command": "specset",
5
+ "args": ["mcp"]
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,10 @@
1
+ # Specset plugin
2
+
3
+ This is the source template for the universal Specset plugin bundle.
4
+
5
+ - Codex and Claude Code discover the bundled skills and start `specset mcp` over stdio. The CLI owns browser/device authentication and direct local file transfer.
6
+ - ChatGPT and other hosted clients connect to Specset's Streamable HTTP MCP endpoint at `https://<host>/mcp`. OAuth 2.1 consent binds the grant to the selected organization; the server uses the same tool registry and adds host-file upload and signed-download tools.
7
+
8
+ `yarn nx run cli:package` copies the canonical `apps/cli/skills` tree into this plugin template under `apps/cli/publish/plugin/specset`. Keep skill source changes in `apps/cli/skills`; do not hand-edit a staged copy.
9
+
10
+ The hosted transport publishes OAuth protected-resource and authorization-server metadata, supports Client ID Metadata Documents with PKCE, and issues short-lived audience-bound access tokens with rotating refresh tokens. First-party clients may continue to use a Specset session bearer token with an explicit organization header.
@@ -0,0 +1,112 @@
1
+ ---
2
+ name: specset
3
+ description: Core CLI for Specset — install or update the CLI, authenticate with a browser or device code, switch organizations, and run GraphQL with `specset api`. Start here for CLI setup or raw GraphQL access, and whenever a specset command fails with auth or org errors.
4
+ allowed-tools: Bash Read AskUserQuestion
5
+ ---
6
+
7
+ # Specset CLI (`specset`)
8
+
9
+ Use this skill to work with a Specset organization from the command line: run GraphQL queries against your data (projects, drawings, specs, submittals, …) and manage authentication and org context.
10
+
11
+ The `specset` CLI handles OAuth login, org switching, and request signing. The exact flag surface of the installed version is authoritative in its own help output — prefer `specset --help` and `specset <command> --help` over memorized flags when something doesn't match this document.
12
+
13
+ ## Skill Family
14
+
15
+ This is the core skill — setup and mechanics only. Domain workflows live in sibling skills:
16
+
17
+ | Skill | Use for |
18
+ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
19
+ | `specset-search` | Finding anything across specs, drawings, submittals, RFIs, documents, and closeout records |
20
+ | `specset-tools` | Calling Specset's own retrieval tools directly (`specset tools`, `specset mcp`) — deeper than search, lighter than the in-app agent |
21
+ | `specset-projects` | Creating projects, uploading spec/drawing PDFs, publishing spec and drawing sets |
22
+ | `specset-submittals` | Submittal lifecycle, attachments, approvers, and AI compliance reviews |
23
+ | `specset-rfis` | RFI logging, tracking, and responses |
24
+ | `specset-closeout` | Assets, locations, products, companies, warranties, and maintenance |
25
+ | `specset-agent` | Delegating deep project questions to Specset's in-app AI agent |
26
+ | `specset-admin` | Org members, invites, and whitelabel branding |
27
+
28
+ `specset skill install --target <claude|codex|chatgpt>` installs and updates the whole family (`specset skill list` shows what's bundled) — if a skill named above is missing from your skills directory, run it for the current agent.
29
+
30
+ ## First-Run Setup
31
+
32
+ Run this when the user installs or updates the skill/plugin, asks to get started with Specset, or when a command fails because the CLI is missing or unauthenticated. Every step is idempotent — skip any that's already satisfied.
33
+
34
+ 1. **Install or update the CLI** (Node.js 20+). Installing a skill/plugin does not upgrade the npm package. If `specset --version` is missing or older than `npm view @specset/cli version`, run `npm install -g @specset/cli@latest` (don't replace a source-linked development CLI whose version is newer than npm). After upgrading, refresh the skills with `specset skill install --target <claude|codex|chatgpt>`.
35
+ 2. **Authenticate** — check with `specset auth status`. If not logged in: `specset login` when a visible browser is available on this machine (tell the user to complete sign-in there while the command waits); `specset login --device` otherwise (SSH, containers, CI, remote/in-app sessions). Device login prints a code and a link (`https://<host>/device?code=XXXX-XXXX`) — show both to the user; the command completes once they approve. Codes expire after 15 minutes; rerun for a fresh one.
36
+ 3. **Pick the active organization** (required for org-scoped queries): `specset org list`, then `specset org use <slug>`. If the user belongs to exactly one org, select it; otherwise ask which to use.
37
+ 4. **Confirm** with `specset auth status`.
38
+
39
+ Credentials persist across sessions in `~/.config/specset/config.yml` (mode 0600) — setup only needs to happen once per machine.
40
+
41
+ ## Running GraphQL
42
+
43
+ Use the `api` subcommand. It accepts the operation via `--query` or stdin, and string variables via `-F key=value` (repeatable). Output is the raw JSON GraphQL response (`{ data, errors }`) — non-zero exit on HTTP errors or `errors[]`.
44
+
45
+ ```bash
46
+ # Simple query
47
+ specset api --query '{ me { id email firstName } }'
48
+
49
+ # Query with an ID variable
50
+ specset api \
51
+ --query 'query($id: ID!) { project(id: $id) { id name } }' \
52
+ -F id=abc-123
53
+
54
+ # Long operation via stdin
55
+ specset api <<'GQL'
56
+ query {
57
+ me { id email orgMembers { id org { id slug name } } }
58
+ }
59
+ GQL
60
+ ```
61
+
62
+ ### Variable Limitations
63
+
64
+ `-F key=value` only sends **string** values. For non-string args (numbers, booleans, enums, input objects, lists), inline literals directly in the operation rather than parameterizing them. Reserve `-F` for IDs and plain strings.
65
+
66
+ ### Schema Discovery
67
+
68
+ The domain skills document the operations that matter for each workflow, but they are not exhaustive — when an operation or argument doesn't match, introspect rather than guess:
69
+
70
+ ```bash
71
+ specset api --query '{ __type(name: "Project") { fields { name type { name kind ofType { name } } } } }'
72
+ ```
73
+
74
+ The same pattern works for input objects (`inputFields`) and the top-level surface (`{ __schema { queryType { fields { name description } } } }`).
75
+
76
+ ## Agent Tools and MCP
77
+
78
+ Beyond raw GraphQL, the CLI exposes Specset's own retrieval tools — semantic and keyword search with actual content to reason over, not just record previews — as direct commands (`specset tools`) and as a native MCP server (`specset mcp`). The `specset-tools` skill owns that workflow: tool listing, project scoping, the `-F` JSON-parsing difference, MCP registration, and the `sb://` citation convention.
79
+
80
+ ## Uploading Files
81
+
82
+ Several workflows attach an uploaded file — submittal PDFs, RFI attachments, project documents, and org logos. Use the streaming upload command; it requests a presigned storage URL, sends the file without buffering it whole, finalizes the upload, and returns the **cloud file id** required by attachment mutations:
83
+
84
+ ```bash
85
+ specset files upload ./document.pdf
86
+ specset files upload ./photo.jpg --type image/jpeg --json
87
+ ```
88
+
89
+ The active organization is used automatically. With `--json`, read the id from `.id`; without it, the command prints the id. The optional `--type` defaults to `application/octet-stream`.
90
+
91
+ Hosted MCP clients that support OpenAI file parameters receive the equivalent `uploadFile` tool. Pass its returned `.cloudFileId` to domain mutations that accept attachment ids; its `.id` is an `sb://file/...` citation. `getFileDownload` returns an MCP resource link backed by a short-lived URL when the hosted client needs bytes back.
92
+
93
+ ## Waiting on Background Processing
94
+
95
+ Document imports and other bulk operations return a `BulkAction` and continue in the background. There are no subscriptions over `specset api` — poll with short sleeps and cap your retries:
96
+
97
+ ```bash
98
+ specset api --query 'query($id: ID!) {
99
+ bulkAction(id: $id) { totalCount completedCount failedCount }
100
+ }' -F id=<bulk-action-id>
101
+ ```
102
+
103
+ Processing large PDF sets takes minutes; poll every 15–30 seconds and tell the user what's in flight rather than blocking silently.
104
+
105
+ ## Safety
106
+
107
+ Confirm with the user before executing mutations that create, update, or delete data. The domain skills note the mutations with non-obvious consequences.
108
+
109
+ ## Troubleshooting
110
+
111
+ - Auth, org, or missing-CLI errors (`command not found`, `Not logged in`, `No active organization selected`, login hangs) — run the matching First-Run Setup step above.
112
+ - A lookup by id returns `null` without an error — the record usually belongs to a different org than the active one; check `specset auth status` and switch with `specset org use <slug>`.
@@ -0,0 +1,112 @@
1
+ ---
2
+ name: specset-admin
3
+ description: Use when administering a Specset organization — inviting and managing members and roles, or applying whitelabel branding (colors, logos) — for org membership, permissions, or appearance tasks.
4
+ allowed-tools: Bash Read AskUserQuestion
5
+ ---
6
+
7
+ # Org Administration
8
+
9
+ Every operation here takes your org id — get it with `{ me { orgMembers { org { id slug name } } } }` if you don't have it. Inline enums, lists, and input objects in the operation text (`-F` is strings-only; Variable Limitations and Schema Discovery live in the `specset` skill). These mutations change real people's access and the whole org's appearance.
10
+
11
+ ## Members
12
+
13
+ Roles (`OrgMemberTeamRole`): `Member`, `Manager`, `Admin`. Statuses (`OrgMemberStatus`): `PendingApproval`, `Active`, `Deactivated`. You need to be an org Admin for most mutations in this skill.
14
+
15
+ List members — filtered flat list or paginated search:
16
+
17
+ ```bash
18
+ specset api --query 'query($orgId: ID!) {
19
+ orgMembers(orgId: $orgId, statuses: [Active]) {
20
+ id role status user { id email firstName lastName }
21
+ }
22
+ }' -F orgId=<org-id>
23
+
24
+ specset api --query 'query($orgId: ID!, $search: String) {
25
+ paginatedOrgMembers(orgId: $orgId, search: $search, first: 20) {
26
+ edges { node { id role status user { email } } }
27
+ pageInfo { hasNextPage endCursor totalCount }
28
+ }
29
+ }' -F orgId=<org-id> -F search="smith"
30
+ ```
31
+
32
+ Manage them (all take the **membership** id from the queries above, not the user id):
33
+
34
+ - `updateOrgMember(id: ID!, role: ..., projectIds: [ID!])` — `role` is required even when unchanged, so resend the current role when only touching `projectIds`. `projectIds` scopes a Member to specific projects; omit it to leave project access alone.
35
+ - `approveOrgMember(id: ID!)` — activate a `PendingApproval` member.
36
+ - `deactivateOrgMember(id: ID!)` / `reactivateOrgMember(id: ID!)` — suspend and restore access. Deactivation is reversible and preserves history — prefer it over any form of member deletion.
37
+
38
+ ```bash
39
+ specset api --query 'mutation($id: ID!) {
40
+ updateOrgMember(id: $id, role: Manager) { id role user { email } }
41
+ }' -F id=<org-member-id>
42
+ ```
43
+
44
+ ## Invites
45
+
46
+ - `orgInvites(orgId: ID!)` / `paginatedOrgInvites(orgId: ID!, first: Int, after: String)` — pending invites; select `{ id email role inviteUrl }`.
47
+ - `createOrgInvite(orgId: ID!, email: String!, role: ..., projectIds: [ID!])` — sends the invite email; `projectIds` (optional, inline the list) pre-scopes a Member to specific projects. Set `role` deliberately — don't default people to more access than they need. Share the returned `inviteUrl` if the email might not land.
48
+ - `updateOrgInvite(id: ID!, role: ..., projectIds: [ID!])` — `role` is required.
49
+ - `deleteOrgInvite(id: ID!)` — revoke a pending invite.
50
+
51
+ ```bash
52
+ specset api --query 'mutation($orgId: ID!, $email: String!) {
53
+ createOrgInvite(orgId: $orgId, email: $email, role: Member) {
54
+ id email role inviteUrl
55
+ }
56
+ }' -F orgId=<org-id> -F email=alex@example.com
57
+ ```
58
+
59
+ ## Branding Your Organization
60
+
61
+ Appearance changes require the **whitelabel** plan feature: without it the mutation is rejected for org admins, and custom branding won't render even if set. If you hit an authorization error on `appearance`, or branding saves but doesn't show, email support@specset.com about whitelabel access.
62
+
63
+ ### Colors
64
+
65
+ Theming hangs off five anchor colors; the app derives the full light and dark palettes from them:
66
+
67
+ | Anchor | Drives |
68
+ |---|---|
69
+ | `brand` | Primary buttons, links, navigation highlights |
70
+ | `magic` | AI/agent accent surfaces |
71
+ | `success` | Positive states |
72
+ | `danger` | Destructive/error states |
73
+ | `warning` | Caution states |
74
+
75
+ Each is a `#rrggbb` hex string. `appearance` is an input object, so inline it. **`updateOrg` replaces the whole appearance object** — include every anchor you want on every call; any you omit are cleared back to the stock theme.
76
+
77
+ ```bash
78
+ specset api --query 'mutation($id: ID!) {
79
+ updateOrg(id: $id, appearance: { anchors: {
80
+ brand: "#003087", magic: "#0085CA", success: "#4B8320",
81
+ danger: "#B71300", warning: "#FF8400"
82
+ } }) {
83
+ id
84
+ appearance { anchors { brand magic success danger warning } }
85
+ }
86
+ }' -F id=<org-id>
87
+ ```
88
+
89
+ `brand` becomes the button and link color on white backgrounds, so it needs to hold white text. Branding applies to every member of the org — confirm the palette with the user before writing it.
90
+
91
+ ### Logos
92
+
93
+ Two slots, both attached by file id on `updateOrg`:
94
+
95
+ - `logoId` — square mark; shown where space is tight (collapsed navigation).
96
+ - `wideLogoId` — horizontal mark + wordmark; shown in the expanded header.
97
+
98
+ Upload the image first following Uploading Files in the `specset` skill to get a file id, then attach:
99
+
100
+ ```bash
101
+ specset api --query 'mutation($id: ID!, $fileId: ID!) {
102
+ updateOrg(id: $id, wideLogoId: $fileId) { id wideLogo { id } }
103
+ }' -F id=<org-id> -F fileId=<file-id>
104
+ ```
105
+
106
+ Use `logoId: $fileId` for the square mark. Passing `null` (inline) clears a slot.
107
+
108
+ ## Safety
109
+
110
+ - For role changes, deactivations, and invite deletions, echo the affected person's email back to the user first — these change real access immediately.
111
+ - `updateOrg` rejects `maxCredits` and `skus` — those fields are managed by Specset. Send only the fields you mean to change (plus the full `appearance` object when changing any part of it).
112
+ - Creating or deleting organizations, SSO, and email-domain management are out of scope for this skill — direct the user to support@specset.com.
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: specset-agent
3
+ description: Ask Specset's in-project AI agent questions that need deep project understanding — schedules, spec interpretation, cross-document analysis — via `specset agent chat`. Use when raw GraphQL isn't enough or when unsure how to answer from data.
4
+ allowed-tools: Bash Read AskUserQuestion
5
+ ---
6
+
7
+ # Specset Agent Chat
8
+
9
+ Specset runs its own project-aware AI agent with full access to a project's specs, drawings, submittals, RFIs, and closeout data. `specset agent chat` sends it a message and waits for the full response — use it for questions that need Specset's own project understanding rather than raw data access.
10
+
11
+ ## When to Delegate vs. Query Directly
12
+
13
+ Delegate to the agent when the answer requires reading and interpreting documents:
14
+
15
+ - Equipment and schedule extraction ("what mechanical equipment is scheduled on level 2?")
16
+ - Spec interpretation ("what are the submittal requirements in section 08 71 00?")
17
+ - Drawing analysis ("which sheets show the roof drainage details?")
18
+ - Drafting a submittal log from the specs (see the `specset-submittals` skill for applying the result)
19
+ - Comparing plan sets or revisions
20
+
21
+ Query directly with `specset api` when the user wants structured records — lists, lookups, creates, updates. The `specset-search`, `specset-projects`, `specset-submittals`, `specset-rfis`, and `specset-closeout` skills cover those workflows.
22
+
23
+ ## Driving a Conversation
24
+
25
+ ```bash
26
+ # Ask a question scoped to a project
27
+ specset agent chat --project <projectId> -m "What mechanical equipment is scheduled on level 2?"
28
+
29
+ # Continue the same conversation (thread id is in the first response)
30
+ specset agent chat --thread <threadId> -m "Which of those have submittals?"
31
+
32
+ # Review a past conversation
33
+ specset agent show <threadId>
34
+ ```
35
+
36
+ Use `--json` for machine-readable output (all commands accept it). Long-running questions may need `--timeout <seconds>` — document analysis can take a few minutes.
37
+
38
+ Find a project id first if you don't have one:
39
+
40
+ ```bash
41
+ specset api --query 'query { projects { id name } }'
42
+ ```
43
+
44
+ ## Notes
45
+
46
+ - Keep one thread per topic: follow-ups via `--thread` give the agent the prior context and produce better answers than fresh threads.
47
+ - The agent may propose changes (creating submittals, updating records) but those are applied through the Specset UI thread where the user approves them — CLI chat is best treated as read/analysis oriented. To make changes headlessly, use the mutations documented in the domain skills instead.
48
+ - The agent answers from the project you scope it to; double-check the `--project` id when results look unrelated.
@@ -0,0 +1,94 @@
1
+ ---
2
+ name: specset-closeout
3
+ description: Use when managing Specset closeout and facilities data — assets, locations, products, companies, warranties, and maintenance tasks, plus their documents and links — for closeout package or O&M/facilities tasks.
4
+ allowed-tools: Bash Read AskUserQuestion
5
+ ---
6
+
7
+ # Closeout & Facilities Data
8
+
9
+ Run every operation with `specset api`. All ids are plain UUIDs. Inline enums, numbers, lists, and input objects in the operation text (`-F` is strings-only; Variable Limitations and Schema Discovery live in the `specset` skill).
10
+
11
+ ## Entity Model
12
+
13
+ **Locations** are the spatial scaffold (site → building → level → room), nesting via `parentId`. **Systems** are disciplines and equipment groups (HVAC, electrical, fire protection), also nested. **Products** are the spec'd make/models. **Assets** are installed instances: each asset sits at one location, is optionally an instance of one product with one responsible **company**, and can join multiple systems through role-tagged memberships. **Warranties** and **maintenance tasks** hang off assets, and maintenance tasks accumulate **completions** (service-log entries). Everything is scoped to a project.
14
+
15
+ ## The Per-Entity Pattern
16
+
17
+ Asset, Location, Product, Company, System, Warranty, and MaintenanceTask share one surface:
18
+
19
+ | Operation | Shape |
20
+ |---|---|
21
+ | `<entity>(id: ID!)` | Single record (`null` if missing or in another org) |
22
+ | `paginated<Entities>(projectId: ID!, limit: Int, offset: Int, ...)` | Returns `{ items { ... } totalFilteredCount }` |
23
+ | `search<Entities>(projectId: ID!, search: String)` | Fuzzy text lookup |
24
+ | `create<Entity>(input: ...)` / `update<Entity>(input: ...)` | Update inputs take `id` plus only the fields to change |
25
+ | `delete<Entity>(id: ID!)` | Hard delete — see Safety |
26
+
27
+ Real deviations:
28
+
29
+ - **Asset** — create requires `projectId` + `locationId`. `paginatedAssets` also takes `search` and a `filters` input (`statuses`, `locationIds`, `productIds`, `systemIds`, `responsibleCompanyIds`, `parentIds`, `archived`) for cross-entity tracing.
30
+ - **Warranty / MaintenanceTask** — asset-scoped: create inputs take `assetId` (not `projectId`); no `search<Entities>` query, and `search` on their paginated lists is ignored — scope by asset with `warrantiesByAsset(assetId)` / `maintenanceTasksByAsset(assetId)` instead.
31
+ - Batch deletes exist for some entities (`deleteLocations(ids)`, `deleteCompanies(ids)`, `deleteSystems(ids)`, `deleteWarranties(ids)`, `deleteMaintenanceTasks(ids)`) — same confirmation rules as single deletes.
32
+
33
+ Introspect the input object before composing any create or update, e.g. `{ __type(name: "CreateAssetInput") { inputFields { name type { name kind ofType { name } } } } }`.
34
+
35
+ Prefer `update<Entity>` for changes: pass `id` plus only what changes; pass `null` to clear an optional relationship (e.g. `productId: null`). Update inputs for Asset, Location, Product, Company, and System accept `archived: true` — a reversible soft-archive that drops the record from default lists. Prefer it over delete.
36
+
37
+ ## Asset Links
38
+
39
+ Three `set*` mutations each **replace** the full id set for the given `(assetId, role)` tuple — anything previously linked at that role and not in the new list is removed; pass `[]` to clear. Inline the enum and the id list:
40
+
41
+ - `setAssetSystems(assetId: ID!, role: ..., systemIds: [ID!]!)` — roles: `Primary`, `Secondary`, `Control`, `MonitoredBy`, `Other`. Read back with `assetSystemMemberships(assetId)`.
42
+ - `setAssetSubmittals(assetId: ID!, relationship: ..., submittalIds: [ID!]!)` — relationships: `Source`, `Revision`, `Substitution`, `Addendum`. Read back with `assetSubmittalLinks(assetId)`.
43
+ - `setAssetDrawingSheets(assetId: ID!, role: ..., drawingSheetIds: [ID!]!)` — roles include `Plan`, `Schedule`, `Diagram`, `Detail`, `AsBuilt`, `Other`. Read back with `assetDrawingSheetLinks(assetId)`.
44
+
45
+ ```bash
46
+ specset api --query 'mutation($assetId: ID!, $systemId: ID!) {
47
+ setAssetSystems(assetId: $assetId, role: Primary, systemIds: [$systemId]) {
48
+ id role system { id name }
49
+ }
50
+ }' -F assetId=<asset-id> -F systemId=<system-id>
51
+ ```
52
+
53
+ ## Entity Documents
54
+
55
+ Each entity links to project documents through a role-tagged pivot. Find document ids with `documents(projectId: ID!) { id title }`. The pattern (uniform across Location, Product, Company, System, Warranty, MaintenanceTask; substitute the entity name):
56
+
57
+ - `<entity>Documents(<entity>Id: ID!)` — list the pivots. Select `{ id roles document { id title } }`.
58
+ - `set<Entity>DocumentRoles(<entity>Id: ID!, documentId: ID!, roles: [...]!)` — upsert the linkage; an empty `roles` list removes it (and the document itself if orphaned). Roles are per-entity enums — e.g. `ProductDocumentRole` is `Datasheet`, `InstallationManual`, `OperationManual`, `MaintenanceManual`, `Warranty`, `SubmittalPackage`, `Other`; introspect each entity's enum before use.
59
+ - `delete<Entity>Document(id: ID!)` — takes the **pivot** id from the list query, not the document id.
60
+
61
+ Asset is the exception: it has `assetDocuments(assetId)` and `deleteAssetDocument(id)` but no set-roles mutation.
62
+
63
+ ## Documentation Blocks
64
+
65
+ Asset, Location, Product, Company, and System each carry a markdown documentation block used by closeout exports:
66
+
67
+ - `update<Entity>Documentation(id: ID!, markdown: String!)` — replace the block. Pass `markdown` via `-F` (it's a plain string).
68
+ - `mark<Entity>DocumentationComplete(id: ID!)` — attest the block is current (clears it from stale-documentation rollups).
69
+
70
+ ## Maintenance Completions
71
+
72
+ Service history lives on completions under each task:
73
+
74
+ - `maintenanceTaskCompletions(maintenanceTaskId: ID!)` — select `{ id performedDate dueDate performedByName result }`.
75
+ - `createMaintenanceTaskCompletion(input: { maintenanceTaskId, performedDate, ... })` — `performedDate` is required; optional `dueDate`, `performedByName`, `performedByCompanyId`, `result`. Introspect `CreateMaintenanceTaskCompletionInput` for the full set.
76
+ - `updateMaintenanceTaskCompletion(input: { id, ... })` / `deleteMaintenanceTaskCompletion(id: ID!)`.
77
+ - Completion documents: `addMaintenanceTaskCompletionDocument(completionId: ID!, documentId: ID!)` and `deleteMaintenanceTaskCompletionDocument(id: ID!)` (pivot id).
78
+
79
+ ## Dedup / Re-Pointing Recipe
80
+
81
+ For duplicate products P1 (loser) and P2 (survivor):
82
+
83
+ 1. `paginatedAssets(projectId: ..., filters: { productIds: ["<p1-id>"] })` — collect affected assets.
84
+ 2. `updateAsset(input: { id: ..., productId: "<p2-id>" })` per asset.
85
+ 3. Re-run step 1 and confirm zero results.
86
+ 4. `updateProduct(input: { id: "<p1-id>", archived: true })` — archive the loser; delete only if it is clearly an unreferenced mistake.
87
+
88
+ The same shape works for duplicate companies, locations, and systems.
89
+
90
+ ## Safety
91
+
92
+ - Deletes here are hard deletes with no undo — name the exact record (tag/name + id) when confirming.
93
+ - Prefer `update` over delete-and-recreate, and `archived: true` over delete: deleting a product, location, or company silently nulls the references on assets that pointed at it. Check impact first with the filtered list queries.
94
+ - Removing a document linkage can delete the underlying document if nothing else references it — say so when confirming.
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: specset-projects
3
+ description: Use when standing up a new Specset project or adding document sets to one — create the project, upload spec/drawing PDFs, monitor processing, and publish spec sets and drawing sets.
4
+ allowed-tools: Bash Read AskUserQuestion
5
+ ---
6
+
7
+ # Specset Projects
8
+
9
+ This skill mutates data — confirm the project name and which files belong to it before creating anything. Inline enums, ints, lists, and input objects in the operation text (`-F` is strings-only; Variable Limitations and Schema Discovery live in the `specset` skill).
10
+
11
+ ## Reading Projects
12
+
13
+ ```graphql
14
+ projects(orgId: ID!, archived: Boolean, includeAll: Boolean = false): [Project!]!
15
+ paginatedProjects(orgId: ID!, search: String, limit: Int, offset: Int, archived: Boolean = false): ProjectPage! # { items totalFilteredCount }
16
+ project(id: ID!): Project!
17
+ ```
18
+
19
+ **`projects` is membership-filtered by default** — it returns only projects where the caller is a project member. Inline `includeAll: true` to list every project in the org. Use `paginatedProjects` with a `search` string to find a project by name.
20
+
21
+ ## Creating a Project
22
+
23
+ `createProject` does **not** add the creator as a project member, so the new project won't appear in the default `projects` list — fetch it by `project(id)`, and add membership as a separate step, mirroring the UI.
24
+
25
+ 1. Create it:
26
+
27
+ ```bash
28
+ specset api --query 'mutation($orgId: ID!, $name: String!) {
29
+ createProject(orgId: $orgId, name: $name) { id name }
30
+ }' -F orgId=<org-id> -F name='Riverside Medical Center'
31
+ ```
32
+
33
+ 2. Get your own user id with `{ me { id } }`, then add yourself (and anyone else the user names) as a member. `role` is an optional `OrgMemberTeamRole` enum — `Member`, `Manager`, or `Admin` — inlined when specified:
34
+
35
+ ```bash
36
+ specset api --query 'mutation($projectId: ID!, $userId: ID!) {
37
+ addProjectMember(projectId: $projectId, userId: $userId, role: Member) { id }
38
+ }' -F projectId=<project-id> -F userId=<user-id>
39
+ ```
40
+
41
+ ## Importing Documents
42
+
43
+ 1. Upload each PDF following Uploading Files in the `specset` skill. Each `completeUpload` returns a **cloud file id** — collect them.
44
+ 2. Register the uploads on the project in a single call. The input object is inlined; ID variables are fine as its leaves:
45
+
46
+ ```bash
47
+ specset api --query 'mutation($projectId: ID!, $f1: ID!, $f2: ID!) {
48
+ createProjectDocuments(input: {
49
+ projectId: $projectId
50
+ cloudFileIds: [$f1, $f2]
51
+ autoImport: true
52
+ }) { id totalCount completedCount failedCount }
53
+ }' -F projectId=<project-id> -F f1=<cloud-file-id> -F f2=<cloud-file-id>
54
+ ```
55
+
56
+ 3. It returns a **BulkAction** — poll it per Waiting on Background Processing in the `specset` skill. Processing extracts each document's outline and content, indexes it for search, and splits it into candidate spec files and drawing files by page range.
57
+
58
+ ### `autoImport`: publish automatically or curate
59
+
60
+ - `autoImport: true` — every generated spec/drawing file is classified and published into default sets automatically. Use when the user wants everything live immediately.
61
+ - Omit it (or `false`) — files land **unpromoted** for manual curation below. Use when the user wants to control set titles, membership, or ordering.
62
+
63
+ Ask the user when their intent is ambiguous — this choice changes what gets published. The input also supports folders: `parentFolderId` / `newFolderName` for flat imports, or `folderTree` + `files` to mirror a directory structure — introspect `CreateProjectDocumentsInput` for those shapes.
64
+
65
+ ## Publishing Spec and Drawing Sets
66
+
67
+ Publishing promotes processed files into a SpecSet or DrawingSet container. After a non-autoImport run finishes:
68
+
69
+ 1. Review what processing generated, and summarize it to the user before publishing:
70
+
71
+ ```graphql
72
+ unpromotedSpecFiles(projectId: ID!): [SpecFile!]! # { id title }
73
+ unpromotedDrawingFiles(projectId: ID!): [DrawingFile!]! # { id title }
74
+ ```
75
+
76
+ Empty lists mean processing hasn't finished, or `autoImport` already promoted everything.
77
+
78
+ 2. Create a set and assign files into it (`issuedAt` is an optional DateTime — inline an ISO literal like `issuedAt: "2026-06-01T00:00:00Z"`):
79
+
80
+ ```bash
81
+ specset api --query 'mutation($projectId: ID!, $title: String!) {
82
+ createSpecSet(title: $title, projectId: $projectId) { id title }
83
+ }' -F projectId=<project-id> -F title='Issued for Construction'
84
+
85
+ specset api --query 'mutation($specSetId: ID!, $f1: ID!) {
86
+ assignSpecFilesToSpecSet(specSetId: $specSetId, specFileIds: [$f1]) { id }
87
+ }' -F specSetId=<spec-set-id> -F f1=<spec-file-id>
88
+ ```
89
+
90
+ The drawing side is symmetric: `createDrawingSet(title: String!, projectId: ID!, issuedAt: DateTime)` then `assignDrawingFilesToDrawingSet(drawingSetId: ID!, drawingFileIds: [ID!]!)`.
91
+
92
+ 3. When a project has multiple sets (e.g. an addendum on top of the base issue), set which one governs. Pass the **complete** list of set ids, highest precedence first:
93
+
94
+ ```graphql
95
+ updateSpecSetPrecedence(projectId: ID!, specSetIds: [ID!]!): [SpecSet!]!
96
+ updateDrawingSetPrecedence(projectId: ID!, drawingSetIds: [ID!]!): [DrawingSet!]!
97
+ ```
98
+
99
+ 4. Verify and summarize — query `project(id)` (not the membership-filtered `projects` list) with `specSets { id title }` and `drawingSets { id title }`, and report what was created.
100
+
101
+ ## Archiving and Deleting
102
+
103
+ - **Prefer `archiveProject(id: ID!)`** — reversible via `unarchiveProject(id: ID!)`. Archived projects drop out of default lists but keep all data.
104
+ - **`deleteProject(id: ID!)` permanently destroys the entire project** — documents, specs, drawings, submittals, RFIs, everything. Confirm with the user, naming the exact project, before running it. If they just want it out of the way, archive instead.
105
+
106
+ ## Troubleshooting
107
+
108
+ - Authorization error on a mutation — the user lacks that permission; the same rules as the UI apply. Surface the message, don't retry.
109
+ - A freshly created project "missing" from `projects` — that's the membership gotcha above, not a failure.
110
+ - A document reports a failed processing status on the BulkAction — report it to the user; re-uploading is their call.