specky-sdd 3.4.0 → 3.5.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/CHANGELOG.md +22 -0
- package/README.md +24 -8
- package/SECURITY.md +15 -8
- package/apm.yml +1 -1
- package/config.yml +1 -1
- package/dist/cli/commands/serve.d.ts.map +1 -1
- package/dist/cli/commands/serve.js +3 -2
- package/dist/cli/commands/serve.js.map +1 -1
- package/dist/cli/index.js +3 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/config.d.ts +21 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +82 -6
- package/dist/config.js.map +1 -1
- package/dist/index.js +63 -14
- package/dist/index.js.map +1 -1
- package/dist/services/audit-logger.d.ts +54 -5
- package/dist/services/audit-logger.d.ts.map +1 -1
- package/dist/services/audit-logger.js +121 -9
- package/dist/services/audit-logger.js.map +1 -1
- package/dist/services/file-manager.d.ts.map +1 -1
- package/dist/services/file-manager.js +9 -3
- package/dist/services/file-manager.js.map +1 -1
- package/dist/tools/audit.js +1 -1
- package/dist/tools/audit.js.map +1 -1
- package/dist/tools/rbac.d.ts.map +1 -1
- package/dist/tools/rbac.js +18 -11
- package/dist/tools/rbac.js.map +1 -1
- package/dist/tools/tool-enforcement.d.ts +15 -1
- package/dist/tools/tool-enforcement.d.ts.map +1 -1
- package/dist/tools/tool-enforcement.js +70 -9
- package/dist/tools/tool-enforcement.js.map +1 -1
- package/dist/utils/token-table.d.ts +41 -0
- package/dist/utils/token-table.d.ts.map +1 -0
- package/dist/utils/token-table.js +103 -0
- package/dist/utils/token-table.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-enforcement.d.ts","sourceRoot":"","sources":["../../src/tools/tool-enforcement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,
|
|
1
|
+
{"version":3,"file":"tool-enforcement.d.ts","sourceRoot":"","sources":["../../src/tools/tool-enforcement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,KAAK,QAAQ,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AASjE,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,WAAW,CAAC;IACzB,UAAU,EAAE,UAAU,CAAC;IACvB,YAAY,EAAE,YAAY,CAAC;CAC5B;AAED,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,OAAO,EAAE,IAAI,CAAC;CACf;AAWD;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,QAAQ,CAAC;CACjB;AAuBD,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAIhD;AAwBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,cAAc,CAajE;AAqLD,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,sBAAsB,GAC9B,IAAI,CASN"}
|
|
@@ -37,11 +37,40 @@ function getToolInput(args) {
|
|
|
37
37
|
const firstArg = args[0];
|
|
38
38
|
return isRecord(firstArg) ? firstArg : {};
|
|
39
39
|
}
|
|
40
|
-
function
|
|
40
|
+
function isRbacRole(value) {
|
|
41
|
+
return value === "viewer" || value === "contributor" || value === "admin";
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Extract the authenticated identity from the MCP request extra (second
|
|
45
|
+
* handler argument). The HTTP transport propagates `req.auth` as
|
|
46
|
+
* `extra.authInfo`; the principal/role live in `authInfo.extra`.
|
|
47
|
+
*/
|
|
48
|
+
export function getCallerIdentity(args) {
|
|
49
|
+
const extra = args[1];
|
|
50
|
+
if (!isRecord(extra) || !isRecord(extra["authInfo"]))
|
|
51
|
+
return {};
|
|
52
|
+
const authInfo = extra["authInfo"];
|
|
53
|
+
const authExtra = isRecord(authInfo["extra"]) ? authInfo["extra"] : {};
|
|
54
|
+
const principal = typeof authExtra["principal"] === "string" && authExtra["principal"]
|
|
55
|
+
? authExtra["principal"]
|
|
56
|
+
: typeof authInfo["clientId"] === "string" && authInfo["clientId"]
|
|
57
|
+
? authInfo["clientId"]
|
|
58
|
+
: undefined;
|
|
59
|
+
const role = isRbacRole(authExtra["role"]) ? authExtra["role"] : undefined;
|
|
60
|
+
return { principal, role };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Role precedence: authenticated identity (token table) > SDD_ROLE env
|
|
64
|
+
* (local/stdio convenience — the launcher owns the process anyway) > config
|
|
65
|
+
* default_role. When a request carries an authenticated identity, the env var
|
|
66
|
+
* is deliberately ignored: a remote caller must not out-vote its token.
|
|
67
|
+
*/
|
|
68
|
+
function getActiveRole(rbacEngine, identity) {
|
|
69
|
+
if (identity.role)
|
|
70
|
+
return identity.role;
|
|
41
71
|
const envRole = process.env["SDD_ROLE"];
|
|
42
|
-
if (envRole
|
|
72
|
+
if (isRbacRole(envRole))
|
|
43
73
|
return envRole;
|
|
44
|
-
}
|
|
45
74
|
return rbacEngine.roleDefault;
|
|
46
75
|
}
|
|
47
76
|
function buildDeniedResponse(payload) {
|
|
@@ -58,6 +87,7 @@ async function auditStart(auditLogger, context, phase) {
|
|
|
58
87
|
feature_number: context.featureNumber,
|
|
59
88
|
phase,
|
|
60
89
|
role: context.activeRole,
|
|
90
|
+
principal: context.principal,
|
|
61
91
|
result: "success",
|
|
62
92
|
summary: "Execution started",
|
|
63
93
|
input_hash: context.inputHash,
|
|
@@ -72,6 +102,7 @@ async function auditSuccess(auditLogger, context, output, phase) {
|
|
|
72
102
|
feature_number: context.featureNumber,
|
|
73
103
|
phase,
|
|
74
104
|
role: context.activeRole,
|
|
105
|
+
principal: context.principal,
|
|
75
106
|
result: "success",
|
|
76
107
|
summary: "Execution completed",
|
|
77
108
|
input_hash: context.inputHash,
|
|
@@ -87,38 +118,56 @@ async function auditError(auditLogger, context, summary, phase) {
|
|
|
87
118
|
feature_number: context.featureNumber,
|
|
88
119
|
phase,
|
|
89
120
|
role: context.activeRole,
|
|
121
|
+
principal: context.principal,
|
|
90
122
|
result: "error",
|
|
91
123
|
summary,
|
|
92
124
|
input_hash: context.inputHash,
|
|
93
125
|
previous_hash: auditLogger.currentHash,
|
|
94
126
|
});
|
|
95
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Post-execution audit failures cannot un-run the tool; even in fail-closed
|
|
130
|
+
* mode the result is returned and the failure is surfaced on stderr. Only the
|
|
131
|
+
* pre-execution entry gates execution.
|
|
132
|
+
*/
|
|
133
|
+
async function auditBestEffort(write, toolName) {
|
|
134
|
+
try {
|
|
135
|
+
await write;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
139
|
+
console.error(`[specky] Post-execution audit write failed for ${toolName}: ${message}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
96
142
|
function wrapToolHandler(toolName, handler, options) {
|
|
97
143
|
return async (...args) => {
|
|
98
144
|
const input = getToolInput(args);
|
|
99
145
|
const specDir = getSpecDir(input);
|
|
100
|
-
const
|
|
146
|
+
const identity = getCallerIdentity(args);
|
|
147
|
+
const activeRole = getActiveRole(options.rbacEngine, identity);
|
|
101
148
|
const context = {
|
|
102
149
|
toolName,
|
|
103
150
|
specDir,
|
|
104
151
|
featureNumber: getFeatureNumber(input),
|
|
105
152
|
activeRole,
|
|
153
|
+
principal: identity.principal,
|
|
106
154
|
inputHash: hashValue(input),
|
|
107
155
|
};
|
|
108
156
|
const rbac = options.rbacEngine.checkAccess(activeRole, toolName);
|
|
109
157
|
if (!rbac.allowed) {
|
|
110
|
-
await auditError(options.auditLogger, context, rbac.reason ?? "RBAC access denied");
|
|
158
|
+
await auditBestEffort(auditError(options.auditLogger, context, rbac.reason ?? "RBAC access denied"), toolName);
|
|
111
159
|
return buildDeniedResponse({
|
|
112
160
|
error: "access_denied",
|
|
113
161
|
tool: toolName,
|
|
114
162
|
active_role: activeRole,
|
|
163
|
+
...(identity.principal ? { principal: identity.principal } : {}),
|
|
115
164
|
message: rbac.reason ?? `Role ${activeRole} cannot invoke ${toolName}.`,
|
|
116
165
|
});
|
|
117
166
|
}
|
|
118
167
|
const phaseCheck = await options.stateMachine.validatePhaseForTool(specDir, toolName);
|
|
119
168
|
if (!phaseCheck.allowed) {
|
|
120
169
|
const message = phaseCheck.error_message ?? `Tool ${toolName} is not allowed in phase ${phaseCheck.current_phase}.`;
|
|
121
|
-
await auditError(options.auditLogger, context, message, phaseCheck.current_phase);
|
|
170
|
+
await auditBestEffort(auditError(options.auditLogger, context, message, phaseCheck.current_phase), toolName);
|
|
122
171
|
return buildDeniedResponse({
|
|
123
172
|
error: "phase_validation_failed",
|
|
124
173
|
tool: toolName,
|
|
@@ -127,15 +176,27 @@ function wrapToolHandler(toolName, handler, options) {
|
|
|
127
176
|
message,
|
|
128
177
|
});
|
|
129
178
|
}
|
|
130
|
-
|
|
179
|
+
// Fail-closed gate: if the pre-execution audit entry cannot be written,
|
|
180
|
+
// the tool does not run (no unaudited actions in enterprise mode).
|
|
181
|
+
try {
|
|
182
|
+
await auditStart(options.auditLogger, context, phaseCheck.current_phase);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
186
|
+
return buildDeniedResponse({
|
|
187
|
+
error: "audit_unavailable",
|
|
188
|
+
tool: toolName,
|
|
189
|
+
message: `Refusing to execute: the audit trail could not be written and audit.fail_closed is on. (${message})`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
131
192
|
try {
|
|
132
193
|
const result = await handler(...args);
|
|
133
|
-
await auditSuccess(options.auditLogger, context, result, phaseCheck.current_phase);
|
|
194
|
+
await auditBestEffort(auditSuccess(options.auditLogger, context, result, phaseCheck.current_phase), toolName);
|
|
134
195
|
return result;
|
|
135
196
|
}
|
|
136
197
|
catch (error) {
|
|
137
198
|
const message = error instanceof Error ? error.message : String(error);
|
|
138
|
-
await auditError(options.auditLogger, context, message, phaseCheck.current_phase);
|
|
199
|
+
await auditBestEffort(auditError(options.auditLogger, context, message, phaseCheck.current_phase), toolName);
|
|
139
200
|
throw error;
|
|
140
201
|
}
|
|
141
202
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-enforcement.js","sourceRoot":"","sources":["../../src/tools/tool-enforcement.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"tool-enforcement.js","sourceRoot":"","sources":["../../src/tools/tool-enforcement.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { RbacRole } from "../services/rbac-engine.js";
|
|
3
|
+
declare const tokenEntrySchema: z.ZodObject<{
|
|
4
|
+
principal: z.ZodString;
|
|
5
|
+
role: z.ZodEnum<{
|
|
6
|
+
viewer: "viewer";
|
|
7
|
+
contributor: "contributor";
|
|
8
|
+
admin: "admin";
|
|
9
|
+
}>;
|
|
10
|
+
token: z.ZodOptional<z.ZodString>;
|
|
11
|
+
token_sha256: z.ZodOptional<z.ZodString>;
|
|
12
|
+
}, z.core.$strict>;
|
|
13
|
+
export type TokenTableEntry = z.infer<typeof tokenEntrySchema>;
|
|
14
|
+
export interface BearerIdentity {
|
|
15
|
+
authorized: boolean;
|
|
16
|
+
/** Set when the token table matched a named token. */
|
|
17
|
+
principal?: string;
|
|
18
|
+
/** Set when the token table matched a named token. */
|
|
19
|
+
role?: RbacRole;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Load and validate the tokens file. Throws on any problem — the caller must
|
|
23
|
+
* treat a configured-but-broken tokens file as fatal (fail-closed).
|
|
24
|
+
*/
|
|
25
|
+
export declare function loadTokenTable(filePath: string): TokenTableEntry[];
|
|
26
|
+
export declare function sha256Hex(value: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the identity behind an Authorization header.
|
|
29
|
+
*
|
|
30
|
+
* - Token table configured → the presented token must match a named entry;
|
|
31
|
+
* returns its principal + role. The shared token is ignored in this mode
|
|
32
|
+
* (named identities only).
|
|
33
|
+
* - Only SDD_HTTP_TOKEN configured → legacy single shared token; authorized
|
|
34
|
+
* requests get principal "shared-token" and no role (RBAC falls back to
|
|
35
|
+
* SDD_ROLE / default_role).
|
|
36
|
+
* - Neither configured → authorized (auth disabled; loopback bind is the
|
|
37
|
+
* remaining guard).
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveBearerIdentity(authorizationHeader: string | undefined, table: readonly TokenTableEntry[], sharedToken: string): BearerIdentity;
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=token-table.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-table.d.ts","sourceRoot":"","sources":["../../src/utils/token-table.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAG3D,QAAA,MAAM,gBAAgB;;;;;;;;;kBAalB,CAAC;AAQL,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE/D,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,OAAO,CAAC;IACpB,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,IAAI,CAAC,EAAE,QAAQ,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,EAAE,CAYlE;AAQD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CACnC,mBAAmB,EAAE,MAAM,GAAG,SAAS,EACvC,KAAK,EAAE,SAAS,eAAe,EAAE,EACjC,WAAW,EAAE,MAAM,GAClB,cAAc,CAuBhB"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* token-table.ts — named bearer tokens for the HTTP transport.
|
|
3
|
+
*
|
|
4
|
+
* Enterprise deployments map each token to an authenticated principal and an
|
|
5
|
+
* RBAC role via a YAML file referenced by SDD_HTTP_TOKENS_FILE. The file must
|
|
6
|
+
* live OUTSIDE the workspace (e.g. /etc/specky/tokens.yml, mode 0600) — the
|
|
7
|
+
* whole point is that workspace writers cannot mint themselves a role.
|
|
8
|
+
*
|
|
9
|
+
* tokens:
|
|
10
|
+
* - principal: alice
|
|
11
|
+
* role: admin
|
|
12
|
+
* token_sha256: "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
|
|
13
|
+
* - principal: ci-bot
|
|
14
|
+
* role: viewer
|
|
15
|
+
* token: "plaintext-also-works" # sha256 form is recommended
|
|
16
|
+
*
|
|
17
|
+
* Loading is fail-closed: a configured file that is missing or malformed
|
|
18
|
+
* throws, so the server refuses to start with broken auth instead of silently
|
|
19
|
+
* accepting everyone.
|
|
20
|
+
*/
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
23
|
+
import { parse } from "yaml";
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
import { isBearerAuthorized } from "./http-auth.js";
|
|
26
|
+
const tokenEntrySchema = z
|
|
27
|
+
.object({
|
|
28
|
+
principal: z.string().min(1),
|
|
29
|
+
role: z.enum(["viewer", "contributor", "admin"]),
|
|
30
|
+
token: z.string().min(8).optional(),
|
|
31
|
+
token_sha256: z
|
|
32
|
+
.string()
|
|
33
|
+
.regex(/^[0-9a-fA-F]{64}$/, "token_sha256 must be a 64-char hex SHA-256")
|
|
34
|
+
.optional(),
|
|
35
|
+
})
|
|
36
|
+
.strict()
|
|
37
|
+
.refine((e) => Boolean(e.token) !== Boolean(e.token_sha256), {
|
|
38
|
+
message: "each token entry needs exactly one of `token` or `token_sha256`",
|
|
39
|
+
});
|
|
40
|
+
const tokenTableSchema = z
|
|
41
|
+
.object({
|
|
42
|
+
tokens: z.array(tokenEntrySchema).min(1),
|
|
43
|
+
})
|
|
44
|
+
.strict();
|
|
45
|
+
/**
|
|
46
|
+
* Load and validate the tokens file. Throws on any problem — the caller must
|
|
47
|
+
* treat a configured-but-broken tokens file as fatal (fail-closed).
|
|
48
|
+
*/
|
|
49
|
+
export function loadTokenTable(filePath) {
|
|
50
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
51
|
+
const parsed = parse(raw) ?? {};
|
|
52
|
+
const result = tokenTableSchema.safeParse(parsed);
|
|
53
|
+
if (!result.success) {
|
|
54
|
+
throw new Error(`Invalid tokens file ${filePath}: ${result.error.issues
|
|
55
|
+
.map((i) => (i.path.length ? `${i.path.join(".")}: ${i.message}` : i.message))
|
|
56
|
+
.join("; ")}`);
|
|
57
|
+
}
|
|
58
|
+
return result.data.tokens;
|
|
59
|
+
}
|
|
60
|
+
function constantTimeEqual(a, b) {
|
|
61
|
+
const bufA = Buffer.from(a);
|
|
62
|
+
const bufB = Buffer.from(b);
|
|
63
|
+
return bufA.length === bufB.length && timingSafeEqual(bufA, bufB);
|
|
64
|
+
}
|
|
65
|
+
export function sha256Hex(value) {
|
|
66
|
+
return createHash("sha256").update(value).digest("hex");
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the identity behind an Authorization header.
|
|
70
|
+
*
|
|
71
|
+
* - Token table configured → the presented token must match a named entry;
|
|
72
|
+
* returns its principal + role. The shared token is ignored in this mode
|
|
73
|
+
* (named identities only).
|
|
74
|
+
* - Only SDD_HTTP_TOKEN configured → legacy single shared token; authorized
|
|
75
|
+
* requests get principal "shared-token" and no role (RBAC falls back to
|
|
76
|
+
* SDD_ROLE / default_role).
|
|
77
|
+
* - Neither configured → authorized (auth disabled; loopback bind is the
|
|
78
|
+
* remaining guard).
|
|
79
|
+
*/
|
|
80
|
+
export function resolveBearerIdentity(authorizationHeader, table, sharedToken) {
|
|
81
|
+
if (table.length === 0) {
|
|
82
|
+
return { authorized: isBearerAuthorized(authorizationHeader, sharedToken) };
|
|
83
|
+
}
|
|
84
|
+
const prefix = "Bearer ";
|
|
85
|
+
if (!authorizationHeader || !authorizationHeader.startsWith(prefix)) {
|
|
86
|
+
return { authorized: false };
|
|
87
|
+
}
|
|
88
|
+
const presented = authorizationHeader.slice(prefix.length);
|
|
89
|
+
const presentedSha = sha256Hex(presented);
|
|
90
|
+
// Evaluate every entry (no early exit) to keep timing independent of match position.
|
|
91
|
+
let matched;
|
|
92
|
+
for (const entry of table) {
|
|
93
|
+
const hit = entry.token_sha256
|
|
94
|
+
? constantTimeEqual(presentedSha, entry.token_sha256.toLowerCase())
|
|
95
|
+
: constantTimeEqual(presented, entry.token ?? "");
|
|
96
|
+
if (hit && !matched)
|
|
97
|
+
matched = entry;
|
|
98
|
+
}
|
|
99
|
+
if (!matched)
|
|
100
|
+
return { authorized: false };
|
|
101
|
+
return { authorized: true, principal: matched.principal, role: matched.role };
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=token-table.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-table.js","sourceRoot":"","sources":["../../src/utils/token-table.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specky-sdd",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "Specky — Spec-Driven Development CLI toolkit. 58 MCP tools, 13 agents, 22 prompts, 8 skills, 16 hooks. Unified `specky` CLI installs to Claude Code or GitHub Copilot on macOS, Linux, and Windows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|