@specset/cli 0.2.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/LICENSE +201 -0
- package/README.md +82 -0
- package/THIRD_PARTY_NOTICES.md +259 -0
- package/dist/main.js +30769 -0
- package/package.json +32 -0
- package/skills/specset/SKILL.md +171 -0
- package/skills/specset-admin/SKILL.md +116 -0
- package/skills/specset-agent/SKILL.md +50 -0
- package/skills/specset-closeout/SKILL.md +114 -0
- package/skills/specset-projects/SKILL.md +113 -0
- package/skills/specset-rfis/SKILL.md +124 -0
- package/skills/specset-search/SKILL.md +131 -0
- package/skills/specset-submittals/SKILL.md +164 -0
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@specset/cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Command-line interface for Specset — authenticate, run GraphQL queries, and chat with Specset project agents.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"homepage": "https://specset.com",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/specsetai/specbook.git",
|
|
10
|
+
"directory": "apps/cli"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"specset",
|
|
14
|
+
"construction",
|
|
15
|
+
"cli",
|
|
16
|
+
"graphql",
|
|
17
|
+
"agent"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"bin": {
|
|
24
|
+
"specset": "./dist/main.js"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"open": "^11.0.0"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: specset
|
|
3
|
+
description: Core CLI for Specset — install, authenticate, switch organizations, and run GraphQL with `specset api`. Start here for any Specset task, 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-projects` | Creating projects, uploading spec/drawing PDFs, publishing spec and drawing sets |
|
|
21
|
+
| `specset-submittals` | Submittal lifecycle, attachments, approvers, and AI compliance reviews |
|
|
22
|
+
| `specset-rfis` | RFI logging, tracking, and responses |
|
|
23
|
+
| `specset-closeout` | Assets, locations, products, companies, warranties, and maintenance |
|
|
24
|
+
| `specset-agent` | Delegating deep project questions to Specset's in-app AI agent |
|
|
25
|
+
| `specset-admin` | Org members, invites, and whitelabel branding |
|
|
26
|
+
|
|
27
|
+
`specset skill install` 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.
|
|
28
|
+
|
|
29
|
+
## First-Run Setup
|
|
30
|
+
|
|
31
|
+
Perform this setup yourself when the user 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.
|
|
32
|
+
|
|
33
|
+
1. Ensure the CLI is installed:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
which specset || npm install -g @specset/cli
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Requires Node.js 20+ with npm. If `npm` itself is missing, stop and ask the user to install Node.js (https://nodejs.org) first.
|
|
40
|
+
|
|
41
|
+
2. Check authentication with `specset auth status`. If not logged in, start the login flow — it opens the user's browser; tell them to complete sign-in there while the command waits:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
specset login
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Login is interactive by design — never try to bypass it or handle credentials directly. On a headless machine (no browser), login isn't possible; direct the user to a machine with a browser. Don't launch a login mid-task without telling the user what's happening.
|
|
48
|
+
|
|
49
|
+
3. Pick the active organization (required for org-scoped queries):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
specset org list
|
|
53
|
+
specset org use <slug>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
If the user belongs to exactly one org, select it; otherwise ask which to use.
|
|
57
|
+
|
|
58
|
+
4. Confirm everything is ready:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
specset auth status
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Credentials persist across sessions in `~/.config/specset/config.yml` (mode 0600) — setup only needs to happen once per machine.
|
|
65
|
+
|
|
66
|
+
## Running GraphQL
|
|
67
|
+
|
|
68
|
+
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[]`.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Simple query
|
|
72
|
+
specset api --query '{ me { id email firstName } }'
|
|
73
|
+
|
|
74
|
+
# Query with an ID variable
|
|
75
|
+
specset api \
|
|
76
|
+
--query 'query($id: ID!) { project(id: $id) { id name } }' \
|
|
77
|
+
-F id=abc-123
|
|
78
|
+
|
|
79
|
+
# Long operation via stdin
|
|
80
|
+
specset api <<'GQL'
|
|
81
|
+
query {
|
|
82
|
+
me { id email orgMembers { id org { id slug name } } }
|
|
83
|
+
}
|
|
84
|
+
GQL
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Variable Limitations
|
|
88
|
+
|
|
89
|
+
`-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.
|
|
90
|
+
|
|
91
|
+
### Schema Discovery
|
|
92
|
+
|
|
93
|
+
Discover the API shape through introspection queries:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Top-level queries
|
|
97
|
+
specset api --query '{ __schema { queryType { fields { name description } } } }'
|
|
98
|
+
|
|
99
|
+
# Fields on a specific type
|
|
100
|
+
specset api --query '{ __type(name: "Project") { fields { name type { name kind ofType { name } } } } }'
|
|
101
|
+
|
|
102
|
+
# Input object shape
|
|
103
|
+
specset api --query '{ __type(name: "CreateSubmittalInput") { inputFields { name type { name kind ofType { name } } } } }'
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
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.
|
|
107
|
+
|
|
108
|
+
## Uploading Files
|
|
109
|
+
|
|
110
|
+
Several workflows attach an uploaded file — submittal PDFs, RFI attachments, project documents, org logos. Files are uploaded directly to storage via a presigned URL, so attaching one is a three-step flow that needs `bash`, `curl`, and `jq`. The result is a **file id** you pass to whatever mutation consumes it.
|
|
111
|
+
|
|
112
|
+
1. Request a presigned upload URL:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
specset api --query 'mutation($orgId: ID!) {
|
|
116
|
+
generateFileUploadUrl(orgId: $orgId) { id url fields { key value } }
|
|
117
|
+
}' -F orgId=<your-org-id> > upload.json
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
2. POST the file to that URL with the returned form fields — the `file` field must come **last**:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
URL=$(jq -r '.data.generateFileUploadUrl.url' upload.json)
|
|
124
|
+
ARGS=()
|
|
125
|
+
while IFS= read -r kv; do ARGS+=(-F "$kv"); done \
|
|
126
|
+
< <(jq -r '.data.generateFileUploadUrl.fields[] | "\(.key)=\(.value)"' upload.json)
|
|
127
|
+
curl -sf -X POST "${ARGS[@]}" -F "file=@document.pdf" "$URL"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
3. Finalize the upload to create the file record and capture its id:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
UPLOAD_ID=$(jq -r '.data.generateFileUploadUrl.id' upload.json)
|
|
134
|
+
FILE_ID=$(specset api --query 'mutation($orgId: ID!, $id: ID!, $filename: String!) {
|
|
135
|
+
completeUpload(orgId: $orgId, id: $id, filename: $filename) { id }
|
|
136
|
+
}' -F orgId=<your-org-id> -F id=$UPLOAD_ID -F filename=document.pdf | jq -r '.data.completeUpload.id')
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Get your org id first if you don't have it:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
specset api --query '{ me { orgMembers { org { id slug name } } } }'
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Waiting on Background Processing
|
|
146
|
+
|
|
147
|
+
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:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
specset api --query 'query($id: ID!) {
|
|
151
|
+
bulkAction(id: $id) { totalCount completedCount failedCount }
|
|
152
|
+
}' -F id=<bulk-action-id>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Processing large PDF sets takes minutes; poll every 15–30 seconds and tell the user what's in flight rather than blocking silently.
|
|
156
|
+
|
|
157
|
+
## Safety Rules
|
|
158
|
+
|
|
159
|
+
- Default to read-only queries.
|
|
160
|
+
- Confirm with the user before executing mutations that create, update, or delete data.
|
|
161
|
+
- Never hardcode credentials in commands — the CLI manages tokens via OAuth.
|
|
162
|
+
- Tokens live in `~/.config/specset/config.yml`; do not echo or copy that file's contents.
|
|
163
|
+
|
|
164
|
+
## Troubleshooting
|
|
165
|
+
|
|
166
|
+
- `command not found: specset` — run the First-Run Setup above.
|
|
167
|
+
- `Not logged in` — run `specset login` (opens the user's browser; tell them to complete sign-in there).
|
|
168
|
+
- `No active organization selected` — run `specset org list`, then `specset org use <slug>`.
|
|
169
|
+
- 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>`.
|
|
170
|
+
- Login requires a browser; on a headless machine, run the CLI from a machine with a browser first, or contact Specset support about headless options.
|
|
171
|
+
- After upgrading the CLI (`npm i -g @specset/cli@latest`), refresh the skills with `specset skill install`.
|
|
@@ -0,0 +1,116 @@
|
|
|
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
|
+
Requires the `specset` CLI, logged in with an active org. If a command fails with `command not found`, `Not logged in`, or `No active organization`, follow First-Run Setup in the `specset` skill.
|
|
10
|
+
|
|
11
|
+
Every operation here takes your org id — get it with `{ me { orgMembers { org { id slug name } } } }` if you don't have it. Remember Variable Limitations in the `specset` skill: inline enums, lists, and input objects as literals in the operation text; reserve `-F` for ids and plain strings. When an operation or argument doesn't match this document, introspect (see the `specset` skill → Schema Discovery) rather than guess. These mutations change real people's access and the whole org's appearance — confirm each one with the user before running it.
|
|
12
|
+
|
|
13
|
+
## Members
|
|
14
|
+
|
|
15
|
+
Roles (`OrgMemberTeamRole`): `Member`, `Manager`, `Admin`. Statuses (`OrgMemberStatus`): `PendingApproval`, `Active`, `Deactivated`. You need to be an org Admin for most mutations in this skill.
|
|
16
|
+
|
|
17
|
+
List members — filtered flat list or paginated search:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
specset api --query 'query($orgId: ID!) {
|
|
21
|
+
orgMembers(orgId: $orgId, statuses: [Active]) {
|
|
22
|
+
id role status user { id email firstName lastName }
|
|
23
|
+
}
|
|
24
|
+
}' -F orgId=<org-id>
|
|
25
|
+
|
|
26
|
+
specset api --query 'query($orgId: ID!, $search: String) {
|
|
27
|
+
paginatedOrgMembers(orgId: $orgId, search: $search, first: 20) {
|
|
28
|
+
edges { node { id role status user { email } } }
|
|
29
|
+
pageInfo { hasNextPage endCursor totalCount }
|
|
30
|
+
}
|
|
31
|
+
}' -F orgId=<org-id> -F search="smith"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Manage them (all take the **membership** id from the queries above, not the user id):
|
|
35
|
+
|
|
36
|
+
- `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.
|
|
37
|
+
- `approveOrgMember(id: ID!)` — activate a `PendingApproval` member.
|
|
38
|
+
- `deactivateOrgMember(id: ID!)` / `reactivateOrgMember(id: ID!)` — suspend and restore access. Deactivation is reversible and preserves history — always prefer it over any form of member deletion.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
specset api --query 'mutation($id: ID!) {
|
|
42
|
+
updateOrgMember(id: $id, role: Manager) { id role user { email } }
|
|
43
|
+
}' -F id=<org-member-id>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Invites
|
|
47
|
+
|
|
48
|
+
- `orgInvites(orgId: ID!)` / `paginatedOrgInvites(orgId: ID!, first: Int, after: String)` — pending invites; select `{ id email role inviteUrl }`.
|
|
49
|
+
- `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.
|
|
50
|
+
- `updateOrgInvite(id: ID!, role: ..., projectIds: [ID!])` — `role` is required.
|
|
51
|
+
- `deleteOrgInvite(id: ID!)` — revoke a pending invite.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
specset api --query 'mutation($orgId: ID!, $email: String!) {
|
|
55
|
+
createOrgInvite(orgId: $orgId, email: $email, role: Member) {
|
|
56
|
+
id email role inviteUrl
|
|
57
|
+
}
|
|
58
|
+
}' -F orgId=<org-id> -F email=alex@example.com
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Branding Your Organization
|
|
62
|
+
|
|
63
|
+
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.
|
|
64
|
+
|
|
65
|
+
### Colors
|
|
66
|
+
|
|
67
|
+
Theming hangs off five anchor colors; the app derives the full light and dark palettes from them:
|
|
68
|
+
|
|
69
|
+
| Anchor | Drives |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `brand` | Primary buttons, links, navigation highlights |
|
|
72
|
+
| `magic` | AI/agent accent surfaces |
|
|
73
|
+
| `success` | Positive states |
|
|
74
|
+
| `danger` | Destructive/error states |
|
|
75
|
+
| `warning` | Caution states |
|
|
76
|
+
|
|
77
|
+
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.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
specset api --query 'mutation($id: ID!) {
|
|
81
|
+
updateOrg(id: $id, appearance: { anchors: {
|
|
82
|
+
brand: "#003087", magic: "#0085CA", success: "#4B8320",
|
|
83
|
+
danger: "#B71300", warning: "#FF8400"
|
|
84
|
+
} }) {
|
|
85
|
+
id
|
|
86
|
+
appearance { anchors { brand magic success danger warning } }
|
|
87
|
+
}
|
|
88
|
+
}' -F id=<org-id>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Pick a `brand` dark enough to hold white text (roughly: not a pastel) — it becomes button and link color on white backgrounds. Branding applies to every member of the org, so confirm the palette with the user before writing it.
|
|
92
|
+
|
|
93
|
+
### Logos
|
|
94
|
+
|
|
95
|
+
Two slots, both attached by file id on `updateOrg`:
|
|
96
|
+
|
|
97
|
+
- `logoId` — square mark; shown where space is tight (collapsed navigation).
|
|
98
|
+
- `wideLogoId` — horizontal mark + wordmark; shown in the expanded header.
|
|
99
|
+
|
|
100
|
+
Upload the image first following Uploading Files in the `specset` skill to get a file id, then attach:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
specset api --query 'mutation($id: ID!, $fileId: ID!) {
|
|
104
|
+
updateOrg(id: $id, wideLogoId: $fileId) { id wideLogo { id } }
|
|
105
|
+
}' -F id=<org-id> -F fileId=<file-id>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Use `logoId: $fileId` for the square mark. Passing `null` (inline) clears a slot.
|
|
109
|
+
|
|
110
|
+
## Safety
|
|
111
|
+
|
|
112
|
+
- Confirm every mutation before running it. For role changes, deactivations, and invite deletions, echo the affected person's email back to the user first — these change real access immediately.
|
|
113
|
+
- Prefer `deactivateOrgMember` over anything destructive; never hard-delete a membership.
|
|
114
|
+
- Never pass `maxCredits` or `skus` to `updateOrg` — those fields are managed by Specset and the mutation will be rejected.
|
|
115
|
+
- On `updateOrg`, send only the fields you mean to change (plus the full `appearance` object when changing any part of it).
|
|
116
|
+
- 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
|
+
Requires the `specset` CLI, logged in with an active org. If a command fails with `command not found`, `Not logged in`, or `No active organization`, follow First-Run Setup in the `specset` skill.
|
|
10
|
+
|
|
11
|
+
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.
|
|
12
|
+
|
|
13
|
+
## When to Delegate vs. Query Directly
|
|
14
|
+
|
|
15
|
+
Delegate to the agent when the answer requires reading and interpreting documents:
|
|
16
|
+
|
|
17
|
+
- Equipment and schedule extraction ("what mechanical equipment is scheduled on level 2?")
|
|
18
|
+
- Spec interpretation ("what are the submittal requirements in section 08 71 00?")
|
|
19
|
+
- Drawing analysis ("which sheets show the roof drainage details?")
|
|
20
|
+
- Drafting a submittal log from the specs (see the `specset-submittals` skill for applying the result)
|
|
21
|
+
- Comparing plan sets or revisions
|
|
22
|
+
|
|
23
|
+
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.
|
|
24
|
+
|
|
25
|
+
## Driving a Conversation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Ask a question scoped to a project
|
|
29
|
+
specset agent chat --project <projectId> -m "What mechanical equipment is scheduled on level 2?"
|
|
30
|
+
|
|
31
|
+
# Continue the same conversation (thread id is in the first response)
|
|
32
|
+
specset agent chat --thread <threadId> -m "Which of those have submittals?"
|
|
33
|
+
|
|
34
|
+
# Review a past conversation
|
|
35
|
+
specset agent show <threadId>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Use `--json` for machine-readable output (all commands accept it). Long-running questions may need `--timeout <seconds>` — document analysis can take a few minutes.
|
|
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,114 @@
|
|
|
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
|
+
Requires the `specset` CLI, logged in with an active org. If a command fails with `command not found`, `Not logged in`, or `No active organization`, follow First-Run Setup in the `specset` skill.
|
|
10
|
+
|
|
11
|
+
Run every operation with `specset api`. All ids are plain UUIDs. Remember Variable Limitations in the `specset` skill: `-F` sends strings only — inline every enum, number, boolean, list, and input object as a literal in the operation text, and reserve `-F` for ids and plain strings. This document is not exhaustive — when an operation or argument doesn't match, introspect (see the `specset` skill → Schema Discovery) rather than guess.
|
|
12
|
+
|
|
13
|
+
## Entity Model
|
|
14
|
+
|
|
15
|
+
**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.
|
|
16
|
+
|
|
17
|
+
## The Per-Entity Pattern
|
|
18
|
+
|
|
19
|
+
Asset, Location, Product, Company, System, Warranty, and MaintenanceTask share one surface:
|
|
20
|
+
|
|
21
|
+
| Operation | Shape |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `<entity>(id: ID!)` | Single record (`null` if missing or in another org) |
|
|
24
|
+
| `paginated<Entities>(projectId: ID!, limit: Int, offset: Int, ...)` | Returns `{ items { ... } totalFilteredCount }` |
|
|
25
|
+
| `search<Entities>(projectId: ID!, search: String)` | Fuzzy text lookup |
|
|
26
|
+
| `create<Entity>(input: ...)` / `update<Entity>(input: ...)` | Update inputs take `id` plus only the fields to change |
|
|
27
|
+
| `delete<Entity>(id: ID!)` | Hard delete — see Safety |
|
|
28
|
+
|
|
29
|
+
Real deviations:
|
|
30
|
+
|
|
31
|
+
- **Asset** — create requires `projectId` + `locationId`. `paginatedAssets` also takes `search` and a `filters` input (`statuses`, `locationIds`, `productIds`, `systemIds`, `responsibleCompanyIds`, `parentIds`, `archived`) for cross-entity tracing.
|
|
32
|
+
- **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.
|
|
33
|
+
- Batch deletes exist for some entities (`deleteLocations(ids)`, `deleteCompanies(ids)`, `deleteSystems(ids)`, `deleteWarranties(ids)`, `deleteMaintenanceTasks(ids)`) — same confirmation rules as single deletes.
|
|
34
|
+
|
|
35
|
+
Introspect the input object before composing any create or update, e.g. `{ __type(name: "CreateAssetInput") { inputFields { name type { name kind ofType { name } } } } }`.
|
|
36
|
+
|
|
37
|
+
Worked example — find, then create:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
specset api --query 'query($projectId: ID!) {
|
|
41
|
+
paginatedAssets(projectId: $projectId, search: "AHU", limit: 20, offset: 0) {
|
|
42
|
+
totalFilteredCount
|
|
43
|
+
items { id tag status location { id name } product { id name } }
|
|
44
|
+
}
|
|
45
|
+
}' -F projectId=<project-id>
|
|
46
|
+
|
|
47
|
+
specset api --query 'mutation($projectId: ID!, $locationId: ID!, $productId: ID!) {
|
|
48
|
+
createAsset(input: {
|
|
49
|
+
projectId: $projectId, locationId: $locationId, productId: $productId,
|
|
50
|
+
tag: "AHU-1", status: Installed, quantity: 1
|
|
51
|
+
}) { id tag status }
|
|
52
|
+
}' -F projectId=<project-id> -F locationId=<location-id> -F productId=<product-id>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
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.
|
|
56
|
+
|
|
57
|
+
## Asset Links
|
|
58
|
+
|
|
59
|
+
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:
|
|
60
|
+
|
|
61
|
+
- `setAssetSystems(assetId: ID!, role: ..., systemIds: [ID!]!)` — roles: `Primary`, `Secondary`, `Control`, `MonitoredBy`, `Other`. Read back with `assetSystemMemberships(assetId)`.
|
|
62
|
+
- `setAssetSubmittals(assetId: ID!, relationship: ..., submittalIds: [ID!]!)` — relationships: `Source`, `Revision`, `Substitution`, `Addendum`. Read back with `assetSubmittalLinks(assetId)`.
|
|
63
|
+
- `setAssetDrawingSheets(assetId: ID!, role: ..., drawingSheetIds: [ID!]!)` — roles include `Plan`, `Schedule`, `Diagram`, `Detail`, `AsBuilt`, `Other`. Read back with `assetDrawingSheetLinks(assetId)`.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
specset api --query 'mutation($assetId: ID!, $systemId: ID!) {
|
|
67
|
+
setAssetSystems(assetId: $assetId, role: Primary, systemIds: [$systemId]) {
|
|
68
|
+
id role system { id name }
|
|
69
|
+
}
|
|
70
|
+
}' -F assetId=<asset-id> -F systemId=<system-id>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Entity Documents
|
|
74
|
+
|
|
75
|
+
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):
|
|
76
|
+
|
|
77
|
+
- `<entity>Documents(<entity>Id: ID!)` — list the pivots. Select `{ id roles document { id title } }`.
|
|
78
|
+
- `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.
|
|
79
|
+
- `delete<Entity>Document(id: ID!)` — takes the **pivot** id from the list query, not the document id.
|
|
80
|
+
|
|
81
|
+
Asset is the exception: it has `assetDocuments(assetId)` and `deleteAssetDocument(id)` but no set-roles mutation.
|
|
82
|
+
|
|
83
|
+
## Documentation Blocks
|
|
84
|
+
|
|
85
|
+
Asset, Location, Product, Company, and System each carry a markdown documentation block used by closeout exports:
|
|
86
|
+
|
|
87
|
+
- `update<Entity>Documentation(id: ID!, markdown: String!)` — replace the block. Pass `markdown` via `-F` (it's a plain string).
|
|
88
|
+
- `mark<Entity>DocumentationComplete(id: ID!)` — attest the block is current (clears it from stale-documentation rollups).
|
|
89
|
+
|
|
90
|
+
## Maintenance Completions
|
|
91
|
+
|
|
92
|
+
Service history lives on completions under each task:
|
|
93
|
+
|
|
94
|
+
- `maintenanceTaskCompletions(maintenanceTaskId: ID!)` — select `{ id performedDate dueDate performedByName result }`.
|
|
95
|
+
- `createMaintenanceTaskCompletion(input: { maintenanceTaskId, performedDate, ... })` — `performedDate` is required; optional `dueDate`, `performedByName`, `performedByCompanyId`, `result`. Introspect `CreateMaintenanceTaskCompletionInput` for the full set.
|
|
96
|
+
- `updateMaintenanceTaskCompletion(input: { id, ... })` / `deleteMaintenanceTaskCompletion(id: ID!)`.
|
|
97
|
+
- Completion documents: `addMaintenanceTaskCompletionDocument(completionId: ID!, documentId: ID!)` and `deleteMaintenanceTaskCompletionDocument(id: ID!)` (pivot id).
|
|
98
|
+
|
|
99
|
+
## Dedup / Re-Pointing Recipe
|
|
100
|
+
|
|
101
|
+
For duplicate products P1 (loser) and P2 (survivor):
|
|
102
|
+
|
|
103
|
+
1. `paginatedAssets(projectId: ..., filters: { productIds: ["<p1-id>"] })` — collect affected assets.
|
|
104
|
+
2. `updateAsset(input: { id: ..., productId: "<p2-id>" })` per asset.
|
|
105
|
+
3. Re-run step 1 and confirm zero results.
|
|
106
|
+
4. `updateProduct(input: { id: "<p1-id>", archived: true })` — archive the loser; delete only if it is clearly an unreferenced mistake.
|
|
107
|
+
|
|
108
|
+
The same shape works for duplicate companies, locations, and systems.
|
|
109
|
+
|
|
110
|
+
## Safety
|
|
111
|
+
|
|
112
|
+
- Confirm with the user before every mutation. For deletes, name the exact record (tag/name + id) and get explicit approval — they are hard deletes with no undo.
|
|
113
|
+
- 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.
|
|
114
|
+
- Removing a document linkage can delete the underlying document if nothing else references it — say so when confirming.
|
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
Requires the `specset` CLI, logged in with an active org. If a command fails with `command not found`, `Not logged in`, or `No active organization`, follow First-Run Setup in the `specset` skill.
|
|
10
|
+
|
|
11
|
+
This skill mutates data — confirm with the user before each mutation, and always confirm the project name and which files belong to it before creating anything. Remember Variable Limitations from the `specset` skill: `-F` sends **strings only** — inline every enum, boolean, int, list, and input object as a literal; reserve `-F` for IDs and plain strings. When an operation or argument here doesn't match, introspect (see Schema Discovery in the `specset` skill).
|
|
12
|
+
|
|
13
|
+
## Reading Projects
|
|
14
|
+
|
|
15
|
+
```graphql
|
|
16
|
+
projects(orgId: ID!, archived: Boolean, includeAll: Boolean = false): [Project!]!
|
|
17
|
+
paginatedProjects(orgId: ID!, search: String, limit: Int, offset: Int, archived: Boolean = false): ProjectPage! # { items totalFilteredCount }
|
|
18
|
+
project(id: ID!): Project!
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**`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.
|
|
22
|
+
|
|
23
|
+
## Creating a Project
|
|
24
|
+
|
|
25
|
+
`createProject` does **not** add the creator as a project member, so the new project won't appear in the default `projects` list. Agents that miss this conclude the creation failed and create duplicates — don't. Fetch the new project by `project(id)`, and add membership as a separate step, mirroring the UI.
|
|
26
|
+
|
|
27
|
+
1. Create it:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
specset api --query 'mutation($orgId: ID!, $name: String!) {
|
|
31
|
+
createProject(orgId: $orgId, name: $name) { id name }
|
|
32
|
+
}' -F orgId=<org-id> -F name='Riverside Medical Center'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
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:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
specset api --query 'mutation($projectId: ID!, $userId: ID!) {
|
|
39
|
+
addProjectMember(projectId: $projectId, userId: $userId, role: Member) { id }
|
|
40
|
+
}' -F projectId=<project-id> -F userId=<user-id>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Importing Documents
|
|
44
|
+
|
|
45
|
+
1. Upload each PDF following Uploading Files in the `specset` skill. Each `completeUpload` returns a **cloud file id** — collect them.
|
|
46
|
+
2. Register the uploads on the project in a single call. The input object is inlined; ID variables are fine as its leaves:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
specset api --query 'mutation($projectId: ID!, $f1: ID!, $f2: ID!) {
|
|
50
|
+
createProjectDocuments(input: {
|
|
51
|
+
projectId: $projectId
|
|
52
|
+
cloudFileIds: [$f1, $f2]
|
|
53
|
+
autoImport: true
|
|
54
|
+
}) { id totalCount completedCount failedCount }
|
|
55
|
+
}' -F projectId=<project-id> -F f1=<cloud-file-id> -F f2=<cloud-file-id>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
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.
|
|
59
|
+
|
|
60
|
+
### `autoImport`: publish automatically or curate
|
|
61
|
+
|
|
62
|
+
- `autoImport: true` — every generated spec/drawing file is classified and published into default sets automatically. Use when the user wants everything live immediately.
|
|
63
|
+
- Omit it (or `false`) — files land **unpromoted** for manual curation below. Use when the user wants to control set titles, membership, or ordering.
|
|
64
|
+
|
|
65
|
+
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.
|
|
66
|
+
|
|
67
|
+
## Publishing Spec and Drawing Sets
|
|
68
|
+
|
|
69
|
+
Publishing promotes processed files into a SpecSet or DrawingSet container. After a non-autoImport run finishes:
|
|
70
|
+
|
|
71
|
+
1. Review what processing generated, and summarize it to the user before publishing:
|
|
72
|
+
|
|
73
|
+
```graphql
|
|
74
|
+
unpromotedSpecFiles(projectId: ID!): [SpecFile!]! # { id title }
|
|
75
|
+
unpromotedDrawingFiles(projectId: ID!): [DrawingFile!]! # { id title }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Empty lists mean processing hasn't finished, or `autoImport` already promoted everything.
|
|
79
|
+
|
|
80
|
+
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"`):
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
specset api --query 'mutation($projectId: ID!, $title: String!) {
|
|
84
|
+
createSpecSet(title: $title, projectId: $projectId) { id title }
|
|
85
|
+
}' -F projectId=<project-id> -F title='Issued for Construction'
|
|
86
|
+
|
|
87
|
+
specset api --query 'mutation($specSetId: ID!, $f1: ID!) {
|
|
88
|
+
assignSpecFilesToSpecSet(specSetId: $specSetId, specFileIds: [$f1]) { id }
|
|
89
|
+
}' -F specSetId=<spec-set-id> -F f1=<spec-file-id>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The drawing side is symmetric: `createDrawingSet(title: String!, projectId: ID!, issuedAt: DateTime)` then `assignDrawingFilesToDrawingSet(drawingSetId: ID!, drawingFileIds: [ID!]!)`.
|
|
93
|
+
|
|
94
|
+
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:
|
|
95
|
+
|
|
96
|
+
```graphql
|
|
97
|
+
updateSpecSetPrecedence(projectId: ID!, specSetIds: [ID!]!): [SpecSet!]!
|
|
98
|
+
updateDrawingSetPrecedence(projectId: ID!, drawingSetIds: [ID!]!): [DrawingSet!]!
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
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.
|
|
102
|
+
|
|
103
|
+
## Archiving and Deleting
|
|
104
|
+
|
|
105
|
+
- **Prefer `archiveProject(id: ID!)`** — reversible via `unarchiveProject(id: ID!)`. Archived projects drop out of default lists but keep all data.
|
|
106
|
+
- **`deleteProject(id: ID!)` permanently destroys the entire project** — documents, specs, drawings, submittals, RFIs, everything. Never run it without explicit user confirmation that names the exact project ("delete Riverside Medical Center — permanently?"). If the user just wants it out of the way, archive instead.
|
|
107
|
+
|
|
108
|
+
## Troubleshooting
|
|
109
|
+
|
|
110
|
+
- Authorization error on a mutation — the user lacks that permission; the same rules as the UI apply. Surface the message, don't retry.
|
|
111
|
+
- A freshly created project "missing" from `projects` — that's the membership gotcha above, not a failure. Check `project(id)` before recreating anything.
|
|
112
|
+
- A document reports a failed processing status on the BulkAction — report it to the user; re-uploading is their call.
|
|
113
|
+
- A lookup by id returns `null` — the record belongs to a different org than the active one; check `specset auth status`.
|