@specset/cli 0.2.0 → 0.3.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/README.md +12 -5
- package/dist/main.js +219 -26
- package/package.json +1 -1
- package/skills/specset/SKILL.md +13 -5
- package/skills/specset-admin/SKILL.md +1 -1
- package/skills/specset-agent/SKILL.md +1 -1
- package/skills/specset-closeout/SKILL.md +1 -1
- package/skills/specset-projects/SKILL.md +1 -1
- package/skills/specset-rfis/SKILL.md +1 -1
- package/skills/specset-search/SKILL.md +1 -1
- package/skills/specset-submittals/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -48,16 +48,23 @@ specset agent chat --project <projectId> -m "What mechanical equipment is schedu
|
|
|
48
48
|
|
|
49
49
|
Run `specset --help` or `specset <command> --help` for the full command reference.
|
|
50
50
|
|
|
51
|
-
## Use with Claude and
|
|
51
|
+
## Use with Claude, Codex, and ChatGPT
|
|
52
52
|
|
|
53
53
|
The package bundles a family of [Agent Skills](https://agentskills.io) that teach AI agents how to drive this CLI safely — a core `specset` skill (setup, auth, GraphQL) plus domain skills for search, project setup, submittals, RFIs, closeout data, agent chat, and org administration:
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
-
# Claude Code: install the whole family into ~/.claude/skills
|
|
57
|
-
specset skill install
|
|
56
|
+
# Claude Code: install the whole family into ~/.claude/skills (the default)
|
|
57
|
+
specset skill install --target claude
|
|
58
|
+
|
|
59
|
+
# Codex or ChatGPT desktop: install into ~/.agents/skills
|
|
60
|
+
specset skill install --target codex
|
|
61
|
+
# `--target chatgpt` is an alias for the same directory
|
|
58
62
|
|
|
59
63
|
# Install a subset
|
|
60
|
-
specset skill install --only specset --only specset-search
|
|
64
|
+
specset skill install --target codex --only specset --only specset-search
|
|
65
|
+
|
|
66
|
+
# Override the destination for another agent or a custom setup
|
|
67
|
+
specset skill install --dir /path/to/skills
|
|
61
68
|
|
|
62
69
|
# See what's bundled
|
|
63
70
|
specset skill list
|
|
@@ -67,7 +74,7 @@ specset skill print
|
|
|
67
74
|
specset skill print specset-submittals
|
|
68
75
|
```
|
|
69
76
|
|
|
70
|
-
Re-run `specset skill install
|
|
77
|
+
Re-run `specset skill install --target <agent>` after upgrading the CLI to refresh the installed copies.
|
|
71
78
|
|
|
72
79
|
## Configuration
|
|
73
80
|
|
package/dist/main.js
CHANGED
|
@@ -28757,6 +28757,52 @@ var drawingComparisonRouting = {
|
|
|
28757
28757
|
]
|
|
28758
28758
|
};
|
|
28759
28759
|
|
|
28760
|
+
// apps/cli/src/probes/scenarios/routing/projectScan.ts
|
|
28761
|
+
var projectScanRouting = {
|
|
28762
|
+
name: "routing/project-scan",
|
|
28763
|
+
tier: "routing",
|
|
28764
|
+
description: "Flash conversation routes an exhaustive-inventory ask through the project-scan skill and pauses at the startProjectScan approval.",
|
|
28765
|
+
preconditionNotes: "target project needs a processed drawing set",
|
|
28766
|
+
turns: [
|
|
28767
|
+
{
|
|
28768
|
+
kind: "message",
|
|
28769
|
+
text: "I need a complete inventory of every fire damper and combination fire/smoke damper shown on the mechanical drawings \u2014 device type, rating, location, and the sheet and note where each is called out. Use sensible defaults for scoping \u2014 do not ask me questions before starting."
|
|
28770
|
+
}
|
|
28771
|
+
],
|
|
28772
|
+
assertions: [
|
|
28773
|
+
expectToolCall("loadSkill", { input: { name: "project-scan" } }),
|
|
28774
|
+
expectToolCall("startProjectScan", {
|
|
28775
|
+
state: "approval-requested",
|
|
28776
|
+
minTimes: 1,
|
|
28777
|
+
maxTimes: 1
|
|
28778
|
+
}),
|
|
28779
|
+
custom("scan is scoped to drawing sheets, not the whole project", (ctx) => {
|
|
28780
|
+
const dispatches = ctx.toolCalls().filter((c) => c.toolName === "startProjectScan");
|
|
28781
|
+
const input = dispatches[0]?.input;
|
|
28782
|
+
const scoped = input?.coverageMode === "drawingSheets" && Boolean(
|
|
28783
|
+
input.sheetNumbers?.length || input.disciplines?.length || input.area || input.masterOnly
|
|
28784
|
+
);
|
|
28785
|
+
return {
|
|
28786
|
+
pass: scoped,
|
|
28787
|
+
evidence: `startProjectScan inputs: ${JSON.stringify(dispatches.map((c) => c.input))}`
|
|
28788
|
+
};
|
|
28789
|
+
}),
|
|
28790
|
+
custom("does not attempt the enumeration inline", (ctx) => {
|
|
28791
|
+
const inlineReads = ctx.toolCalls().filter(
|
|
28792
|
+
(c) => ["searchDrawingSheets", "getDrawingSheetContent"].includes(
|
|
28793
|
+
c.toolName
|
|
28794
|
+
)
|
|
28795
|
+
);
|
|
28796
|
+
return {
|
|
28797
|
+
pass: inlineReads.length <= 6,
|
|
28798
|
+
evidence: `${inlineReads.length} inline drawing reads`
|
|
28799
|
+
};
|
|
28800
|
+
}),
|
|
28801
|
+
expectAwaitingApproval("startProjectScan"),
|
|
28802
|
+
expectNoError()
|
|
28803
|
+
]
|
|
28804
|
+
};
|
|
28805
|
+
|
|
28760
28806
|
// apps/cli/src/probes/scenarios/routing/submittalReview.ts
|
|
28761
28807
|
var submittalReviewRouting = {
|
|
28762
28808
|
name: "routing/submittal-review",
|
|
@@ -28903,6 +28949,7 @@ var allScenarios = [
|
|
|
28903
28949
|
submittalReviewRouting,
|
|
28904
28950
|
drawingComparisonRouting,
|
|
28905
28951
|
contractReviewRouting,
|
|
28952
|
+
projectScanRouting,
|
|
28906
28953
|
userTableSkillRouting,
|
|
28907
28954
|
userDocumentSkillRouting,
|
|
28908
28955
|
contractReviewFullLifecycle,
|
|
@@ -30125,6 +30172,12 @@ ${authorizeUrl}
|
|
|
30125
30172
|
try {
|
|
30126
30173
|
await open(authorizeUrl.toString());
|
|
30127
30174
|
} catch {
|
|
30175
|
+
process.stderr.write(
|
|
30176
|
+
`Could not open a browser automatically. Open the URL above manually.
|
|
30177
|
+
If your browser is on a different machine (SSH, container), press Ctrl-C
|
|
30178
|
+
and run \`specset login --device\` instead.
|
|
30179
|
+
`
|
|
30180
|
+
);
|
|
30128
30181
|
}
|
|
30129
30182
|
let callback;
|
|
30130
30183
|
try {
|
|
@@ -30158,6 +30211,101 @@ ${authorizeUrl}
|
|
|
30158
30211
|
return { userId: user.id, email: user.email, host, accessToken };
|
|
30159
30212
|
}
|
|
30160
30213
|
|
|
30214
|
+
// apps/cli/src/oauth/deviceFlow.ts
|
|
30215
|
+
var startDeviceAuthorizationDocument = graphql(`
|
|
30216
|
+
mutation Cli_startOAuthDeviceAuthorization($clientId: String!) {
|
|
30217
|
+
startOAuthDeviceAuthorization(clientId: $clientId) {
|
|
30218
|
+
deviceCode
|
|
30219
|
+
userCode
|
|
30220
|
+
expiresIn
|
|
30221
|
+
interval
|
|
30222
|
+
}
|
|
30223
|
+
}
|
|
30224
|
+
`);
|
|
30225
|
+
var exchangeDeviceCodeDocument = graphql(`
|
|
30226
|
+
mutation Cli_exchangeOAuthDeviceCode(
|
|
30227
|
+
$clientId: String!
|
|
30228
|
+
$deviceCode: String!
|
|
30229
|
+
) {
|
|
30230
|
+
exchangeOAuthDeviceCode(clientId: $clientId, deviceCode: $deviceCode) {
|
|
30231
|
+
accessToken
|
|
30232
|
+
user {
|
|
30233
|
+
id
|
|
30234
|
+
email
|
|
30235
|
+
}
|
|
30236
|
+
}
|
|
30237
|
+
}
|
|
30238
|
+
`);
|
|
30239
|
+
function sleep3(ms) {
|
|
30240
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30241
|
+
}
|
|
30242
|
+
function deviceErrorCode(err) {
|
|
30243
|
+
if (!(err instanceof ClientError)) {
|
|
30244
|
+
return null;
|
|
30245
|
+
}
|
|
30246
|
+
const errors = err.response.errors ?? [];
|
|
30247
|
+
for (const e3 of errors) {
|
|
30248
|
+
const code = e3.extensions?.code;
|
|
30249
|
+
if (typeof code === "string") {
|
|
30250
|
+
return code;
|
|
30251
|
+
}
|
|
30252
|
+
}
|
|
30253
|
+
return null;
|
|
30254
|
+
}
|
|
30255
|
+
async function runDeviceLoginFlow(host) {
|
|
30256
|
+
const client = createGraphQLClient({ host });
|
|
30257
|
+
const start = await client.request(startDeviceAuthorizationDocument, {
|
|
30258
|
+
clientId: OAUTH_CLIENT_ID
|
|
30259
|
+
});
|
|
30260
|
+
const { deviceCode, userCode, expiresIn, interval } = start.startOAuthDeviceAuthorization;
|
|
30261
|
+
const verificationUrl = `${resolveBaseUrl(host)}/device?code=${encodeURIComponent(userCode)}`;
|
|
30262
|
+
process.stderr.write(
|
|
30263
|
+
`To sign in, open this link in a browser on any device:
|
|
30264
|
+
|
|
30265
|
+
${verificationUrl}
|
|
30266
|
+
|
|
30267
|
+
and approve the code ${userCode}.
|
|
30268
|
+
|
|
30269
|
+
Waiting for approval (code expires in ${Math.round(expiresIn / 60)} minutes)...
|
|
30270
|
+
`
|
|
30271
|
+
);
|
|
30272
|
+
let intervalMs = interval * 1e3;
|
|
30273
|
+
const deadline = Date.now() + expiresIn * 1e3;
|
|
30274
|
+
while (Date.now() < deadline) {
|
|
30275
|
+
await sleep3(intervalMs);
|
|
30276
|
+
try {
|
|
30277
|
+
const result = await client.request(exchangeDeviceCodeDocument, {
|
|
30278
|
+
clientId: OAUTH_CLIENT_ID,
|
|
30279
|
+
deviceCode
|
|
30280
|
+
});
|
|
30281
|
+
const { accessToken, user } = result.exchangeOAuthDeviceCode;
|
|
30282
|
+
const current = await loadConfig();
|
|
30283
|
+
const next = setHostConfig(current, host, { token: accessToken });
|
|
30284
|
+
await saveConfig({ ...next, currentHost: host });
|
|
30285
|
+
return { userId: user.id, email: user.email, host, accessToken };
|
|
30286
|
+
} catch (err) {
|
|
30287
|
+
const code = deviceErrorCode(err);
|
|
30288
|
+
if (code === "authorization_pending") {
|
|
30289
|
+
continue;
|
|
30290
|
+
}
|
|
30291
|
+
if (code === "slow_down") {
|
|
30292
|
+
intervalMs += 5e3;
|
|
30293
|
+
continue;
|
|
30294
|
+
}
|
|
30295
|
+
if (code === "expired_token") {
|
|
30296
|
+
break;
|
|
30297
|
+
}
|
|
30298
|
+
if (code === "access_denied") {
|
|
30299
|
+
throw new Error("Authorization was denied.");
|
|
30300
|
+
}
|
|
30301
|
+
throw err;
|
|
30302
|
+
}
|
|
30303
|
+
}
|
|
30304
|
+
throw new Error(
|
|
30305
|
+
`The code expired before it was approved. Run \`specset login --device\` again to get a new one.`
|
|
30306
|
+
);
|
|
30307
|
+
}
|
|
30308
|
+
|
|
30161
30309
|
// apps/cli/src/commands/login.ts
|
|
30162
30310
|
async function loginCommand(flags) {
|
|
30163
30311
|
const host = resolveActiveHost(this.config, flags.host);
|
|
@@ -30167,7 +30315,7 @@ async function loginCommand(flags) {
|
|
|
30167
30315
|
`Already logged in to ${host}. Run \`${logoutCmd}\` first.`
|
|
30168
30316
|
);
|
|
30169
30317
|
}
|
|
30170
|
-
const result = await runLoginFlow(host);
|
|
30318
|
+
const result = flags.device ? await runDeviceLoginFlow(host) : await runLoginFlow(host);
|
|
30171
30319
|
printResult(
|
|
30172
30320
|
this.process,
|
|
30173
30321
|
flags.json ?? false,
|
|
@@ -30186,6 +30334,11 @@ var login = buildCommand({
|
|
|
30186
30334
|
brief: "Host to authenticate against (e.g. app.specset.dev, app2.specset.dev, http://localhost:3000). Added to ~/.config/specset/config.yml.",
|
|
30187
30335
|
optional: true
|
|
30188
30336
|
},
|
|
30337
|
+
device: {
|
|
30338
|
+
kind: "boolean",
|
|
30339
|
+
brief: "Sign in without a local browser: prints a code you approve from any device (SSH, containers, CI)",
|
|
30340
|
+
optional: true
|
|
30341
|
+
},
|
|
30189
30342
|
json: {
|
|
30190
30343
|
kind: "boolean",
|
|
30191
30344
|
brief: "Emit machine-readable JSON instead of human output",
|
|
@@ -30430,9 +30583,12 @@ import path5 from "node:path";
|
|
|
30430
30583
|
// apps/cli/src/skillAsset.ts
|
|
30431
30584
|
import { existsSync as existsSync2, readdirSync, readFileSync } from "node:fs";
|
|
30432
30585
|
var CORE_SKILL_NAME = "specset";
|
|
30586
|
+
var SKILL_VERSION_FILENAME = ".specset-version";
|
|
30433
30587
|
var skillsRoot = new URL("../skills/", import.meta.url);
|
|
30434
30588
|
function listPackagedSkillNames() {
|
|
30435
|
-
return readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter(
|
|
30589
|
+
return readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter(
|
|
30590
|
+
(entry) => existsSync2(new URL(`${entry.name}/SKILL.md`, skillsRoot))
|
|
30591
|
+
).map((entry) => entry.name).sort((a3, b) => {
|
|
30436
30592
|
if (a3 === CORE_SKILL_NAME) {
|
|
30437
30593
|
return -1;
|
|
30438
30594
|
}
|
|
@@ -30445,11 +30601,6 @@ function listPackagedSkillNames() {
|
|
|
30445
30601
|
function readPackagedSkill(name2) {
|
|
30446
30602
|
return readFileSync(new URL(`${name2}/SKILL.md`, skillsRoot), "utf8");
|
|
30447
30603
|
}
|
|
30448
|
-
function stampSkillVersion(content, version) {
|
|
30449
|
-
return content.replace(/^---\n/, `---
|
|
30450
|
-
version: ${version}
|
|
30451
|
-
`);
|
|
30452
|
-
}
|
|
30453
30604
|
function extractSkillVersion(content) {
|
|
30454
30605
|
return /^version: (.+)$/m.exec(content)?.[1]?.trim() ?? null;
|
|
30455
30606
|
}
|
|
@@ -30468,27 +30619,51 @@ function cliVersion() {
|
|
|
30468
30619
|
}
|
|
30469
30620
|
|
|
30470
30621
|
// apps/cli/src/commands/skill/install.ts
|
|
30471
|
-
|
|
30472
|
-
|
|
30473
|
-
|
|
30474
|
-
|
|
30622
|
+
var SKILL_TARGET_PATHS = {
|
|
30623
|
+
claude: [".claude", "skills"],
|
|
30624
|
+
codex: [".agents", "skills"],
|
|
30625
|
+
chatgpt: [".agents", "skills"]
|
|
30626
|
+
};
|
|
30627
|
+
function isSkillInstallTarget(value2) {
|
|
30628
|
+
return Object.hasOwn(SKILL_TARGET_PATHS, value2);
|
|
30629
|
+
}
|
|
30630
|
+
function defaultSkillsDir(target, homeDir = os2.homedir()) {
|
|
30631
|
+
return path5.join(homeDir, ...SKILL_TARGET_PATHS[target]);
|
|
30632
|
+
}
|
|
30633
|
+
async function readOptionalText(file) {
|
|
30475
30634
|
try {
|
|
30476
|
-
|
|
30635
|
+
return await fs2.readFile(file, "utf8");
|
|
30477
30636
|
} catch (err) {
|
|
30478
|
-
if (err.code
|
|
30479
|
-
|
|
30637
|
+
if (err.code === "ENOENT") {
|
|
30638
|
+
return null;
|
|
30480
30639
|
}
|
|
30640
|
+
throw err;
|
|
30481
30641
|
}
|
|
30482
|
-
|
|
30642
|
+
}
|
|
30643
|
+
async function installOne(skillsDir, name2, version) {
|
|
30644
|
+
const destDir = path5.join(skillsDir, name2);
|
|
30645
|
+
const destFile = path5.join(destDir, "SKILL.md");
|
|
30646
|
+
const versionFile = path5.join(destDir, SKILL_VERSION_FILENAME);
|
|
30647
|
+
const desired = readPackagedSkill(name2);
|
|
30648
|
+
const [existing, recordedVersion] = await Promise.all([
|
|
30649
|
+
readOptionalText(destFile),
|
|
30650
|
+
readOptionalText(versionFile)
|
|
30651
|
+
]);
|
|
30652
|
+
const previousVersion = recordedVersion?.trim() || (existing ? extractSkillVersion(existing) : null);
|
|
30653
|
+
if (existing === desired && previousVersion === version) {
|
|
30483
30654
|
return { name: name2, path: destFile, action: "current", previousVersion: null };
|
|
30484
30655
|
}
|
|
30485
|
-
await fs2.mkdir(
|
|
30486
|
-
await
|
|
30656
|
+
await fs2.mkdir(destDir, { recursive: true });
|
|
30657
|
+
await Promise.all([
|
|
30658
|
+
fs2.writeFile(destFile, desired),
|
|
30659
|
+
fs2.writeFile(versionFile, `${version}
|
|
30660
|
+
`)
|
|
30661
|
+
]);
|
|
30487
30662
|
return {
|
|
30488
30663
|
name: name2,
|
|
30489
30664
|
path: destFile,
|
|
30490
30665
|
action: existing ? "updated" : "installed",
|
|
30491
|
-
previousVersion: existing ?
|
|
30666
|
+
previousVersion: existing ? previousVersion : null
|
|
30492
30667
|
};
|
|
30493
30668
|
}
|
|
30494
30669
|
async function findOrphanedSkills(skillsDir, packagedNames) {
|
|
@@ -30508,6 +30683,13 @@ async function findOrphanedSkills(skillsDir, packagedNames) {
|
|
|
30508
30683
|
continue;
|
|
30509
30684
|
}
|
|
30510
30685
|
try {
|
|
30686
|
+
const recordedVersion = await readOptionalText(
|
|
30687
|
+
path5.join(skillsDir, entry.name, SKILL_VERSION_FILENAME)
|
|
30688
|
+
);
|
|
30689
|
+
if (recordedVersion?.trim()) {
|
|
30690
|
+
orphans.push(path5.join(skillsDir, entry.name));
|
|
30691
|
+
continue;
|
|
30692
|
+
}
|
|
30511
30693
|
const content = await fs2.readFile(
|
|
30512
30694
|
path5.join(skillsDir, entry.name, "SKILL.md"),
|
|
30513
30695
|
"utf8"
|
|
@@ -30521,7 +30703,14 @@ async function findOrphanedSkills(skillsDir, packagedNames) {
|
|
|
30521
30703
|
return orphans;
|
|
30522
30704
|
}
|
|
30523
30705
|
async function skillInstallCommand(flags) {
|
|
30524
|
-
const
|
|
30706
|
+
const targetName = flags.target?.trim() || "claude";
|
|
30707
|
+
if (!isSkillInstallTarget(targetName)) {
|
|
30708
|
+
return new Error(
|
|
30709
|
+
`Unknown target "${targetName}". Available: ${Object.keys(SKILL_TARGET_PATHS).join(", ")}`
|
|
30710
|
+
);
|
|
30711
|
+
}
|
|
30712
|
+
const target = targetName;
|
|
30713
|
+
const skillsDir = flags.dir?.trim() || defaultSkillsDir(target);
|
|
30525
30714
|
const version = cliVersion();
|
|
30526
30715
|
const packagedNames = listPackagedSkillNames();
|
|
30527
30716
|
let names = packagedNames;
|
|
@@ -30561,7 +30750,7 @@ async function skillInstallCommand(flags) {
|
|
|
30561
30750
|
printResult(
|
|
30562
30751
|
this.process,
|
|
30563
30752
|
flags.json ?? false,
|
|
30564
|
-
{ dir: skillsDir, version, skills: results, orphans },
|
|
30753
|
+
{ target, dir: skillsDir, version, skills: results, orphans },
|
|
30565
30754
|
lines.join("\n")
|
|
30566
30755
|
);
|
|
30567
30756
|
}
|
|
@@ -30570,10 +30759,16 @@ var skillInstall = buildCommand({
|
|
|
30570
30759
|
parameters: {
|
|
30571
30760
|
positional: { kind: "tuple", parameters: [] },
|
|
30572
30761
|
flags: {
|
|
30762
|
+
target: {
|
|
30763
|
+
kind: "parsed",
|
|
30764
|
+
parse: String,
|
|
30765
|
+
brief: "Agent target: claude (~/.claude/skills), codex or chatgpt (~/.agents/skills); defaults to claude",
|
|
30766
|
+
optional: true
|
|
30767
|
+
},
|
|
30573
30768
|
dir: {
|
|
30574
30769
|
kind: "parsed",
|
|
30575
30770
|
parse: String,
|
|
30576
|
-
brief: "
|
|
30771
|
+
brief: "Override the target skills directory",
|
|
30577
30772
|
optional: true
|
|
30578
30773
|
},
|
|
30579
30774
|
only: {
|
|
@@ -30591,7 +30786,7 @@ var skillInstall = buildCommand({
|
|
|
30591
30786
|
}
|
|
30592
30787
|
},
|
|
30593
30788
|
docs: {
|
|
30594
|
-
brief: "Install the Specset agent skills
|
|
30789
|
+
brief: "Install the Specset agent skills for Claude, Codex, or ChatGPT (re-run after upgrading to refresh)"
|
|
30595
30790
|
}
|
|
30596
30791
|
});
|
|
30597
30792
|
|
|
@@ -30631,9 +30826,7 @@ async function skillPrintCommand(_flags, name2) {
|
|
|
30631
30826
|
`Unknown skill "${skillName}". Available: ${available.join(", ")}`
|
|
30632
30827
|
);
|
|
30633
30828
|
}
|
|
30634
|
-
this.process.stdout.write(
|
|
30635
|
-
stampSkillVersion(readPackagedSkill(skillName), cliVersion())
|
|
30636
|
-
);
|
|
30829
|
+
this.process.stdout.write(readPackagedSkill(skillName));
|
|
30637
30830
|
}
|
|
30638
30831
|
var skillPrint = buildCommand({
|
|
30639
30832
|
func: skillPrintCommand,
|
|
@@ -30652,7 +30845,7 @@ var skillPrint = buildCommand({
|
|
|
30652
30845
|
flags: {}
|
|
30653
30846
|
},
|
|
30654
30847
|
docs: {
|
|
30655
|
-
brief: "Print a bundled Specset agent skill to stdout
|
|
30848
|
+
brief: "Print a bundled Specset agent skill to stdout for custom setups"
|
|
30656
30849
|
}
|
|
30657
30850
|
});
|
|
30658
30851
|
|
package/package.json
CHANGED
package/skills/specset/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: specset
|
|
3
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
|
|
4
|
+
allowed-tools: Bash Read AskUserQuestion
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Specset CLI (`specset`)
|
|
@@ -24,7 +24,7 @@ This is the core skill — setup and mechanics only. Domain workflows live in si
|
|
|
24
24
|
| `specset-agent` | Delegating deep project questions to Specset's in-app AI agent |
|
|
25
25
|
| `specset-admin` | Org members, invites, and whitelabel branding |
|
|
26
26
|
|
|
27
|
-
`specset skill install
|
|
27
|
+
`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.
|
|
28
28
|
|
|
29
29
|
## First-Run Setup
|
|
30
30
|
|
|
@@ -44,7 +44,15 @@ Requires Node.js 20+ with npm. If `npm` itself is missing, stop and ask the user
|
|
|
44
44
|
specset login
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
Login is interactive by design — never try to bypass it or handle credentials directly.
|
|
47
|
+
Login is interactive by design — never try to bypass it or handle credentials directly. Don't launch a login mid-task without telling the user what's happening.
|
|
48
|
+
|
|
49
|
+
**On a headless machine** (container, SSH, CI — anywhere the browser isn't on this machine), use the device flow instead:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
specset login --device
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
It prints a short code and a link (`https://<host>/device?code=XXXX-XXXX`). Show both to the user and ask them to open the link on any device and approve; the command completes on its own once they do. The code expires after 15 minutes — if it does, run the command again for a fresh one.
|
|
48
56
|
|
|
49
57
|
3. Pick the active organization (required for org-scoped queries):
|
|
50
58
|
|
|
@@ -167,5 +175,5 @@ Processing large PDF sets takes minutes; poll every 15–30 seconds and tell the
|
|
|
167
175
|
- `Not logged in` — run `specset login` (opens the user's browser; tell them to complete sign-in there).
|
|
168
176
|
- `No active organization selected` — run `specset org list`, then `specset org use <slug>`.
|
|
169
177
|
- 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
|
|
171
|
-
- After upgrading the CLI (`npm i -g @specset/cli@latest`), refresh the skills with `specset skill install
|
|
178
|
+
- Login hangs or the browser can't reach this machine (SSH, container, CI) — use `specset login --device`: it prints a code + link the user approves from any device.
|
|
179
|
+
- After upgrading the CLI (`npm i -g @specset/cli@latest`), refresh the skills with `specset skill install --target <claude|codex|chatgpt>`.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: specset-admin
|
|
3
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
|
|
4
|
+
allowed-tools: Bash Read AskUserQuestion
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Org Administration
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: specset-agent
|
|
3
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
|
|
4
|
+
allowed-tools: Bash Read AskUserQuestion
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Specset Agent Chat
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: specset-closeout
|
|
3
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
|
|
4
|
+
allowed-tools: Bash Read AskUserQuestion
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Closeout & Facilities Data
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: specset-projects
|
|
3
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
|
|
4
|
+
allowed-tools: Bash Read AskUserQuestion
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Specset Projects
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: specset-rfis
|
|
3
3
|
description: Manage Specset RFIs — list, search, create, update, and record responses including the official answer of record. Use for any RFI logging or tracking task.
|
|
4
|
-
allowed-tools: Bash
|
|
4
|
+
allowed-tools: Bash Read AskUserQuestion
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Specset RFIs
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: specset-search
|
|
3
3
|
description: Use when the user asks to find, look up, or locate anything in their Specset projects — semantic and keyword search across specs, drawing sheets, submittals, RFIs, documents, and closeout records.
|
|
4
|
-
allowed-tools: Bash
|
|
4
|
+
allowed-tools: Bash Read AskUserQuestion
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Specset Search
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: specset-submittals
|
|
3
3
|
description: Manage Specset submittals — list, search, create, revise, attach files, track approvers, and run AI spec-compliance reviews. Use for any submittal log or submittal review task.
|
|
4
|
-
allowed-tools: Bash
|
|
4
|
+
allowed-tools: Bash Read AskUserQuestion
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Specset Submittals
|