@specset/cli 0.5.1 → 1.0.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,8 +1,8 @@
1
1
  {
2
2
  "name": "@specset/cli",
3
- "version": "0.5.1",
3
+ "version": "1.0.0",
4
4
  "description": "Command-line interface for Specset — authenticate, run GraphQL queries, and chat with Specset project agents.",
5
- "license": "Apache-2.0",
5
+ "license": "Elastic-2.0",
6
6
  "homepage": "https://specset.com",
7
7
  "repository": {
8
8
  "type": "git",
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "specset",
3
+ "version": "1.0.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": "Elastic-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": "1.0.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": "Elastic-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,11 @@
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
+ - External tool and GraphQL source references are presented as clickable `https://app.specbook.ai/go/...` links. Bundled skills instruct agents to include relevant returned links in answers so users can open the cited source in Specset.
8
+
9
+ `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.
10
+
11
+ 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,120 @@
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 preserves the raw JSON GraphQL envelope (`{ 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
+ ### IDs and Source Links
67
+
68
+ - GraphQL fields named `id` are ordinary UUIDs. Use those UUIDs for GraphQL `ID` variables.
69
+ - Citation-bearing fields named `url` and source references embedded in text are emitted as clickable `https://app.specbook.ai/go/<type>/<uuid>` links. The CLI accepts a complete Specset `/go` link in an **`ID`-typed variable** and extracts its UUID automatically (any focus parameters on the link are dropped, with a warning on stderr). Links inlined as literals in the query text, or passed in non-ID variables, are sent to the server verbatim — inline bare UUIDs there instead.
70
+ - `-F` values for non-ID variables are always sent byte-for-byte as typed. Storing a `/go` link in a text field is fine — it is a real URL.
71
+ - Response output rewrites Specset's internal reference representation to public `/go` links by default; `specset api --refs internal` returns the byte-exact server response for low-level debugging and should not be used in user-facing answers.
72
+ - Include relevant returned `/go` links as descriptive Markdown source links in answers so the user can open the exact source in Specset. Cite only links actually returned by Specset; never manufacture one from an unrelated UUID.
73
+
74
+ ### Schema Discovery
75
+
76
+ 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:
77
+
78
+ ```bash
79
+ specset api --query '{ __type(name: "Project") { fields { name type { name kind ofType { name } } } } }'
80
+ ```
81
+
82
+ The same pattern works for input objects (`inputFields`) and the top-level surface (`{ __schema { queryType { fields { name description } } } }`).
83
+
84
+ ## Agent Tools and MCP
85
+
86
+ 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 clickable Specset source links.
87
+
88
+ ## Uploading Files
89
+
90
+ 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:
91
+
92
+ ```bash
93
+ specset files upload ./document.pdf
94
+ specset files upload ./photo.jpg --type image/jpeg --json
95
+ ```
96
+
97
+ 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`.
98
+
99
+ 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 a clickable `https://app.specbook.ai/go/file/...` source link. `getFileDownload` returns an MCP resource link backed by a short-lived URL when the hosted client needs bytes back.
100
+
101
+ ## Waiting on Background Processing
102
+
103
+ 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:
104
+
105
+ ```bash
106
+ specset api --query 'query($id: ID!) {
107
+ bulkAction(id: $id) { totalCount completedCount failedCount }
108
+ }' -F id=<bulk-action-id>
109
+ ```
110
+
111
+ Processing large PDF sets takes minutes; poll every 15–30 seconds and tell the user what's in flight rather than blocking silently.
112
+
113
+ ## Safety
114
+
115
+ Confirm with the user before executing mutations that create, update, or delete data. The domain skills note the mutations with non-obvious consequences.
116
+
117
+ ## Troubleshooting
118
+
119
+ - 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.
120
+ - 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,50 @@
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
+ Agent transcripts expose their source references as clickable `https://app.specbook.ai/go/...` links. Preserve the most relevant returned links as descriptive Markdown citations when relaying the answer to the user. Do not fabricate additional links from UUIDs. An identifier that is not an `https://` link (a thread-local short id or `doc:`-prefixed identifier) is internal — pass it back into commands verbatim, but never present it to the user as a clickable link. Both `--project` and `--thread` accept returned Specset links as well as UUIDs.
39
+
40
+ Find a project id first if you don't have one:
41
+
42
+ ```bash
43
+ specset api --query 'query { projects { id name } }'
44
+ ```
45
+
46
+ ## Notes
47
+
48
+ - Keep one thread per topic: follow-ups via `--thread` give the agent the prior context and produce better answers than fresh threads.
49
+ - 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.
50
+ - The agent answers from the project you scope it to; double-check the `--project` id when results look unrelated.
@@ -0,0 +1,188 @@
1
+ ---
2
+ name: specset-closeout
3
+ description: Use when managing Specset closeout and facilities data — assets, locations, products, companies, warranties, procedures, maintenance plans, work orders, and their documents and links.
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` sends 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 through `parentId`. **Systems** are disciplines and equipment groups, also nested. **Products** are specified make/models. **Assets** are installed instances: each asset has a location, may reference a product and responsible company, and can join multiple systems. **Warranties** attach to assets.
14
+
15
+ Maintenance has three layers:
16
+
17
+ - **Procedure** — reusable checklist content: ordered typed fields, descriptions, and source references.
18
+ - **MaintenancePlan** — a schedule on exactly one Product, Asset, Location, or System. Product plans fan out to installed assets. Its `tiers` are independent schedule items that pair a Procedure with a calendar cadence, meter trigger, or both.
19
+ - **WorkOrder** — a concrete occurrence or ad-hoc job. Generated work orders snapshot their checklist fields, so history remains stable when a Procedure changes.
20
+
21
+ `MaintenanceTask` and its completion API no longer exist. Use Procedure, MaintenancePlan, and WorkOrder operations below.
22
+
23
+ ## Discover Before Mutating
24
+
25
+ Input objects evolve, especially checklist fields and plan tiers. Introspect the relevant input before composing a mutation:
26
+
27
+ ```bash
28
+ specset api --query '{
29
+ createPlan: __type(name: "CreateMaintenancePlanInput") {
30
+ inputFields { name type { kind name ofType { kind name } } }
31
+ }
32
+ tier: __type(name: "MaintenancePlanTierInput") {
33
+ inputFields { name type { kind name ofType { kind name } } }
34
+ }
35
+ complete: __type(name: "CompleteWorkOrderInput") {
36
+ inputFields { name type { kind name ofType { kind name } } }
37
+ }
38
+ }'
39
+ ```
40
+
41
+ Inline input objects and enums in the operation text. Use `-F` for UUIDs, dates, and other scalar strings.
42
+
43
+ ## Core Closeout Records
44
+
45
+ Asset, Location, Product, Company, System, and Warranty retain the standard surface:
46
+
47
+ | Operation | Shape |
48
+ | ------------------------------------------------------------------- | ------------------------------------------- |
49
+ | `<entity>(id: ID!)` | Single record; nullable when missing |
50
+ | `paginated<Entities>(projectId: ID!, limit: Int, offset: Int, ...)` | `{ items { ... } totalFilteredCount }` |
51
+ | `create<Entity>(input: ...)` / `update<Entity>(input: ...)` | Update inputs take `id` plus changed fields |
52
+ | `delete<Entity>(id: ID!)` | Hard delete; follow Safety below |
53
+
54
+ Notable differences:
55
+
56
+ - Asset creation requires `projectId` and `locationId`. `paginatedAssets` accepts `search` and filters such as `statuses`, `locationIds`, `productIds`, `systemIds`, `responsibleCompanyIds`, `parentIds`, and `archived`.
57
+ - Warranty is asset-scoped; use `warrantiesByAsset(assetId)` to browse one asset's warranties.
58
+ - Location, Product, Company, System, and Asset updates support reversible `archived` state. Prefer archiving to deletion.
59
+ - Batch-delete availability varies. Introspect the mutation root rather than guessing an operation name.
60
+
61
+ Asset link setters replace the complete set for the given role/relationship; omitted ids are removed and `[]` clears the set:
62
+
63
+ - `setAssetSystems(assetId, role, systemIds)` / `assetSystemMemberships(assetId)`
64
+ - `setAssetSubmittals(assetId, relationship, submittalIds)` / `assetSubmittalLinks(assetId)`
65
+ - `setAssetDrawingSheets(assetId, role, drawingSheetIds)` / `assetDrawingSheetLinks(assetId)`
66
+
67
+ ## Procedures
68
+
69
+ Use `procedure(id)` and `paginatedProcedures(projectId, filters, limit, offset)`. Procedure list filters include `search` and `archived`.
70
+
71
+ Create a reusable checklist with its fields atomically:
72
+
73
+ ```bash
74
+ specset api --query 'mutation($projectId: ID!) {
75
+ createProcedure(input: {
76
+ projectId: $projectId
77
+ name: "Quarterly AHU service"
78
+ fields: [
79
+ { name: "Inspect belts", fieldType: Checkbox }
80
+ { name: "Record supply temperature", fieldType: Number, unit: "°F" }
81
+ ]
82
+ }) {
83
+ id name updatedAt
84
+ fields { id name fieldType position required }
85
+ }
86
+ }' -F projectId=<project-id>
87
+ ```
88
+
89
+ The fields' array order sets their `position`; neither `CreateProcedureFieldInput` nor `UpdateProcedureFieldInput` accepts a `position` property. `updateProcedure(input: { id, ... })` is partial for scalar fields. Supplying `fields` performs a full checklist replacement; first read the current fields and include the `id` of every row that should retain its identity. `deleteProcedure(id, force)` is guarded when active plans use it. Do not set `force: true` without naming the affected plans and confirming the destructive change.
90
+
91
+ ## Maintenance Plans and Tiers
92
+
93
+ Read with `maintenancePlan(id)`, `paginatedMaintenancePlans(projectId, filters, limit, offset)`, or `maintenancePlansForAsset(assetId, includeProductPlans)`.
94
+
95
+ Create requires exactly one subject id (`productId`, `assetId`, `locationId`, or `systemId`) and a `frequencyKind`. Prefer `tiers`; the top-level procedure and interval fields remain a single-item compatibility surface. Supply either:
96
+
97
+ - single-tier compatibility fields: `procedureId` (or inline `procedure`) plus `intervalValue` / `intervalUnit`; or
98
+ - `tiers: [MaintenancePlanTierInput!]` for independent schedule items. Each item selects one `procedureId` or creates one inline.
99
+
100
+ Do not mix `tiers` with the single-tier procedure/cadence fields. Calendar item units are `Days`, `Weeks`, `Months`, or `Years`. Optional meter conditions live on each item as the pair `meterUnit` (`Hours` or `Cycles`) and `meterThreshold`; provide both or neither. Calendar cadences are independent and do not need to be multiples of one another. Set `skipLowerTiersOnSharedDate: true` to suppress a shorter calendar item when it naturally shares a date with a longer one; each work order still contains only its own Procedure. A suppressed item's meter condition stays armed and may create that item's work order later. `gateOnOpen` prevents repeated meter-generated work while an occurrence remains open.
101
+
102
+ ```bash
103
+ specset api --query 'mutation($assetId: ID!, $monthlyId: ID!, $quarterlyId: ID!) {
104
+ createMaintenancePlan(input: {
105
+ assetId: $assetId
106
+ name: "AHU PM program"
107
+ frequencyKind: Calendar
108
+ skipLowerTiersOnSharedDate: true
109
+ gateOnOpen: true
110
+ tiers: [
111
+ { procedureId: $monthlyId, intervalValue: 1, intervalUnit: Months }
112
+ {
113
+ procedureId: $quarterlyId
114
+ intervalValue: 3
115
+ intervalUnit: Months
116
+ meterUnit: Hours
117
+ meterThreshold: 500
118
+ }
119
+ ]
120
+ }) {
121
+ id name frequencyKind skipLowerTiersOnSharedDate gateOnOpen
122
+ tiers {
123
+ id intervalValue intervalUnit meterUnit meterThreshold
124
+ procedure { id name }
125
+ }
126
+ openWorkOrderCount
127
+ }
128
+ }' -F assetId=<asset-id> -F monthlyId=<procedure-id> -F quarterlyId=<procedure-id>
129
+ ```
130
+
131
+ For a meter-only plan, prefer the typed input: `schedule: { meter: { unit: Hours|Cycles, threshold: <n>, gateOnOpen } }` with a single tier. The legacy encoding (`frequencyKind: RuntimeHours` or `Cycles`) additionally requires plan-level `intervalValue` set to the threshold and `intervalUnit: Hours`/`Cycles` — a threshold on the item alone is rejected. For an on-demand plan, use `AsNeeded`, `EventTriggered`, or `Other` and omit calendar and meter fields.
132
+
133
+ `updateMaintenancePlan(input: { id, ... })` can retarget the subject: pass exactly one of `productId`/`assetId`/`locationId`/`systemId` (same project). Open work orders for the old subject are retired — untouched ones removed, in-progress ones canceled — and fresh chains generate for the new subject. Supplying `tiers` fully replaces the schedule-item set: include the `id` of every existing item to update/retain, omit an item only when it should be removed, and omit `id` only for new items. Read the plan and its item ids immediately before updating.
134
+
135
+ Use `createWorkOrderFromPlan(planId, targetAssetId, dueDate, coveredTierIds)` for an early/manual occurrence of a plan. Product plans require the concrete `targetAssetId`. `coveredTierIds` is optional; when supplied it states exactly which tiers the occurrence performs.
136
+
137
+ ## Work Orders
138
+
139
+ Use `workOrder(id)`, `paginatedWorkOrders(projectId, filters, limit, offset)`, and `paginatedGroupedWorkOrders(projectId, groupBy, groupKeys, filters, limit, offset)`. Grouped reads require both the grouping dimensions and the current group-key path. Useful fields include `status`, `dueDate`, `performedDate`, `priority`, `assignee`, `plan`, `planTier`, `coveredTiers`, `fields`, and `documents`.
140
+
141
+ `createWorkOrder(input)` creates ad-hoc work and requires exactly one of `assetId`, `locationId`, or `systemId`, plus `name`. Optional `fields` create a reusable inline Procedure; optional `recurrence` also creates a MaintenancePlan. Use `updateWorkOrder(input: { id, ... })` for editable metadata.
142
+
143
+ Checklist responses use `setWorkOrderFieldResponse(input)` or `setWorkOrderFieldResponses(inputs)`. Introspect `SetWorkOrderFieldResponseInput`: answer columns vary by field type (`valueNumber`, `valueText`, `valueChoice`, `status`, and `note`).
144
+
145
+ Complete work through the batch mutation even for one row:
146
+
147
+ ```bash
148
+ specset api --query 'mutation($id: ID!, $performedDate: DateTime!) {
149
+ completeWorkOrders(inputs: [{ id: $id, performedDate: $performedDate }]) {
150
+ workOrder { id status performedDate }
151
+ nextWorkOrder { id status dueDate }
152
+ }
153
+ }' -F id=<work-order-id> -F performedDate=2026-08-04
154
+ ```
155
+
156
+ Other lifecycle operations:
157
+
158
+ - `skipWorkOrder(id, notes, mode)` — closes the occurrence as skipped; omit `mode` for normal independent-item plans. Non-default modes exist only for historical merged work orders.
159
+ - `reopenWorkOrder(id)` — reopens completed/skipped work when allowed.
160
+ - `makeWorkOrderRecurring(workOrderId, recurrence)` — converts eligible ad-hoc work into a recurring plan.
161
+ - `deleteWorkOrder(id)` — hard delete.
162
+ - `bulkAssignWorkOrders(selection, assigneeId)` and `bulkDeleteWorkOrders(selection)` operate on an explicit selection object.
163
+ - `addWorkOrderDocument(workOrderId, documentId, workOrderFieldId)` and `removeWorkOrderDocument(id)` manage evidence. Removal takes the WorkOrderDocument pivot id.
164
+
165
+ ## Entity Documents and Documentation
166
+
167
+ Location, Product, Company, System, and Warranty link project Documents through entity-specific role-tagged pivots. Read `<entity>Documents(<entity>Id)`, then call `set<Entity>DocumentRoles(<entity>Id, documentId, roles)` after introspecting that entity's role enum. An empty role list removes the pivot and may delete the underlying Document when it becomes orphaned. Asset is the exception: it exposes `assetDocuments(assetId)` and `deleteAssetDocument(id)`, but no set-roles mutation.
168
+
169
+ Asset, Location, Product, Company, and System also have markdown documentation blocks. `update<Entity>Documentation(id, markdown)` replaces the block; `mark<Entity>DocumentationComplete(id)` attests that it is current.
170
+
171
+ ## Dedup / Re-pointing
172
+
173
+ For duplicate products P1 (loser) and P2 (survivor):
174
+
175
+ 1. Query `paginatedAssets(projectId, filters: { productIds: [P1] })` and collect impacted assets.
176
+ 2. `updateAsset(input: { id, productId: P2 })` for each asset.
177
+ 3. Re-run the filtered query and confirm zero remaining references.
178
+ 4. Archive P1 with `updateProduct(input: { id: P1, archived: true })`.
179
+
180
+ Use the same impact-first pattern for duplicate companies, locations, and systems.
181
+
182
+ ## Safety
183
+
184
+ - Deletes are hard and have no undo. Before executing, name the exact record and id and confirm the impact.
185
+ - Prefer partial updates, and prefer `archived: true` to delete where supported.
186
+ - Read Procedure fields and MaintenancePlan tiers before any full-replacement update.
187
+ - Deleting plans or force-deleting Procedures can alter future generated work. Check active plan/open-work-order counts first.
188
+ - Removing a document linkage can delete an orphaned underlying Document; disclose that before confirming.