kspec 1.0.15 → 1.0.17

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +417 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kspec",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "Spec-driven development workflow for Kiro CLI",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const os = require('os');
3
4
  const { execSync, spawn } = require('child_process');
4
5
  const readline = require('readline');
5
6
 
@@ -7,6 +8,8 @@ const KSPEC_DIR = '.kspec';
7
8
  const STEERING_DIR = '.kiro/steering';
8
9
  const AGENTS_DIR = '.kiro/agents';
9
10
  const CONFIG_FILE = path.join(KSPEC_DIR, 'config.json');
11
+ const UPDATE_CHECK_FILE = path.join(os.homedir(), '.kspec-update-check');
12
+ const KIRO_MCP_CONFIG = path.join(os.homedir(), '.kiro', 'mcp.json');
10
13
 
11
14
  // Default config
12
15
  const defaultConfig = {
@@ -33,6 +36,123 @@ function saveConfig(cfg) {
33
36
  }
34
37
 
35
38
  const config = loadConfig();
39
+ const pkg = require('../package.json');
40
+
41
+ // Update check (non-blocking, cached for 24h)
42
+ function shouldCheckUpdate() {
43
+ try {
44
+ if (fs.existsSync(UPDATE_CHECK_FILE)) {
45
+ const lastCheck = parseInt(fs.readFileSync(UPDATE_CHECK_FILE, 'utf8'), 10);
46
+ const hoursSinceCheck = (Date.now() - lastCheck) / (1000 * 60 * 60);
47
+ return hoursSinceCheck >= 24;
48
+ }
49
+ } catch {}
50
+ return true;
51
+ }
52
+
53
+ function saveUpdateCheck() {
54
+ try {
55
+ fs.writeFileSync(UPDATE_CHECK_FILE, Date.now().toString());
56
+ } catch {}
57
+ }
58
+
59
+ function compareVersions(v1, v2) {
60
+ const parts1 = v1.split('.').map(Number);
61
+ const parts2 = v2.split('.').map(Number);
62
+ for (let i = 0; i < 3; i++) {
63
+ if ((parts1[i] || 0) < (parts2[i] || 0)) return -1;
64
+ if ((parts1[i] || 0) > (parts2[i] || 0)) return 1;
65
+ }
66
+ return 0;
67
+ }
68
+
69
+ async function checkForUpdates() {
70
+ if (!shouldCheckUpdate()) return;
71
+
72
+ try {
73
+ const https = require('https');
74
+ const data = await new Promise((resolve, reject) => {
75
+ const req = https.get('https://registry.npmjs.org/kspec/latest', { timeout: 3000 }, res => {
76
+ let body = '';
77
+ res.on('data', chunk => body += chunk);
78
+ res.on('end', () => resolve(body));
79
+ });
80
+ req.on('error', reject);
81
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
82
+ });
83
+
84
+ const latest = JSON.parse(data).version;
85
+ saveUpdateCheck();
86
+
87
+ if (compareVersions(pkg.version, latest) < 0) {
88
+ console.log(`\n Update available: ${pkg.version} → ${latest}`);
89
+ console.log(` Run: npm install -g kspec\n`);
90
+ }
91
+ } catch {
92
+ // Silently fail - don't block user workflow
93
+ }
94
+ }
95
+
96
+ // MCP Integration Detection
97
+ function getMcpConfig() {
98
+ try {
99
+ if (fs.existsSync(KIRO_MCP_CONFIG)) {
100
+ return JSON.parse(fs.readFileSync(KIRO_MCP_CONFIG, 'utf8'));
101
+ }
102
+ } catch {}
103
+ return null;
104
+ }
105
+
106
+ function hasAtlassianMcp() {
107
+ const mcpConfig = getMcpConfig();
108
+ if (!mcpConfig || !mcpConfig.mcpServers) return false;
109
+
110
+ // Check for atlassian or jira MCP server
111
+ const serverNames = Object.keys(mcpConfig.mcpServers);
112
+ return serverNames.some(name =>
113
+ name.toLowerCase().includes('atlassian') ||
114
+ name.toLowerCase().includes('jira')
115
+ );
116
+ }
117
+
118
+ function getAtlassianMcpName() {
119
+ const mcpConfig = getMcpConfig();
120
+ if (!mcpConfig || !mcpConfig.mcpServers) return null;
121
+
122
+ const serverNames = Object.keys(mcpConfig.mcpServers);
123
+ return serverNames.find(name =>
124
+ name.toLowerCase().includes('atlassian') ||
125
+ name.toLowerCase().includes('jira')
126
+ );
127
+ }
128
+
129
+ function requireAtlassianMcp() {
130
+ if (!hasAtlassianMcp()) {
131
+ die(`Atlassian MCP not configured.
132
+
133
+ To use Jira integration, you need to:
134
+ 1. Install the Atlassian MCP server
135
+ 2. Configure it in ~/.kiro/mcp.json
136
+
137
+ Example ~/.kiro/mcp.json:
138
+ {
139
+ "mcpServers": {
140
+ "atlassian": {
141
+ "command": "npx",
142
+ "args": ["-y", "@anthropic/mcp-atlassian"],
143
+ "env": {
144
+ "ATLASSIAN_HOST": "https://your-domain.atlassian.net",
145
+ "ATLASSIAN_EMAIL": "your-email@example.com",
146
+ "ATLASSIAN_API_TOKEN": "your-api-token"
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ Get your API token: https://id.atlassian.com/manage-profile/security/api-tokens`);
153
+ }
154
+ return getAtlassianMcpName();
155
+ }
36
156
 
37
157
  // Helpers
38
158
  function log(msg) { console.log(`[kspec] ${msg}`); }
@@ -225,6 +345,15 @@ No active spec. Run: \`kspec spec "Feature Name"\`
225
345
  memory = fs.readFileSync(memoryFile, 'utf8');
226
346
  }
227
347
 
348
+ // Read Jira links if exists
349
+ const jiraLinksFile = path.join(current, 'jira-links.json');
350
+ let jiraLinks = null;
351
+ if (fs.existsSync(jiraLinksFile)) {
352
+ try {
353
+ jiraLinks = JSON.parse(fs.readFileSync(jiraLinksFile, 'utf8'));
354
+ } catch {}
355
+ }
356
+
228
357
  // Build context
229
358
  let content = `# kspec Context
230
359
  > Auto-generated. Always read this first after context compression.
@@ -261,6 +390,24 @@ ${memory}
261
390
  `;
262
391
  }
263
392
 
393
+ if (jiraLinks) {
394
+ content += `## Jira Links
395
+ `;
396
+ if (jiraLinks.sourceIssues && jiraLinks.sourceIssues.length > 0) {
397
+ content += `- Source Issues: ${jiraLinks.sourceIssues.join(', ')}
398
+ `;
399
+ }
400
+ if (jiraLinks.specIssue) {
401
+ content += `- Spec Issue: ${jiraLinks.specIssue}
402
+ `;
403
+ }
404
+ if (jiraLinks.subtasks && jiraLinks.subtasks.length > 0) {
405
+ content += `- Subtasks: ${jiraLinks.subtasks.join(', ')}
406
+ `;
407
+ }
408
+ content += '\n';
409
+ }
410
+
264
411
  content += `## Quick Commands
265
412
  - \`kspec build\` - Continue building tasks
266
413
  - \`kspec verify\` - Verify implementation
@@ -509,6 +656,59 @@ Your job:
509
656
  Output: APPROVE / REQUEST_CHANGES with specific issues.`,
510
657
  keyboardShortcut: 'ctrl+shift+r',
511
658
  welcomeMessage: 'Ready to review. What should I look at?'
659
+ },
660
+
661
+ 'kspec-jira.json': {
662
+ name: 'kspec-jira',
663
+ description: 'Jira integration for specs',
664
+ model: 'claude-sonnet-4',
665
+ tools: ['read', 'write', 'mcp'],
666
+ allowedTools: ['read', 'write', 'mcp'],
667
+ resources: [
668
+ 'file://.kspec/CONTEXT.md',
669
+ 'file://.kiro/steering/**/*.md',
670
+ 'file://.kspec/**/*.md'
671
+ ],
672
+ prompt: `You are the kspec Jira integration agent.
673
+
674
+ PREREQUISITE: This agent requires Atlassian MCP to be configured.
675
+ If MCP calls fail, inform the user to configure Atlassian MCP first.
676
+
677
+ CAPABILITIES:
678
+
679
+ 1. PULL FROM JIRA (when user provides issue keys):
680
+ - Use MCP to fetch Jira issue details
681
+ - Extract: summary, description, acceptance criteria, comments
682
+ - For multiple issues, consolidate into unified requirements
683
+ - Create spec.md with proper attribution to source issues
684
+ - Include Jira links in spec for traceability
685
+
686
+ 2. SYNC TO JIRA (when user asks to sync/push):
687
+ - Create new "Technical Specification" issue in Jira
688
+ - Or update existing issue with spec content
689
+ - Link to source stories
690
+ - Add comment requesting BA review
691
+ - Set appropriate labels (kspec, technical-spec)
692
+
693
+ 3. CREATE SUBTASKS (when user asks after tasks.md exists):
694
+ - Read tasks.md from current spec
695
+ - Create Jira sub-tasks for each task
696
+ - Link to parent spec issue
697
+ - Include task details and acceptance criteria
698
+
699
+ WORKFLOW:
700
+ 1. Read .kspec/CONTEXT.md for current spec state
701
+ 2. Identify what user wants (pull/sync/subtasks)
702
+ 3. Use Atlassian MCP for Jira operations
703
+ 4. Update .kspec/CONTEXT.md with Jira links
704
+ 5. Report what was created/updated
705
+
706
+ IMPORTANT:
707
+ - Always include Jira issue links in spec.md
708
+ - Add "Source: JIRA-XXX" attribution for pulled requirements
709
+ - Update CONTEXT.md with linked Jira issues`,
710
+ keyboardShortcut: 'ctrl+shift+j',
711
+ welcomeMessage: 'Jira integration ready. Provide issue keys to pull, or say "sync" to push spec to Jira.'
512
712
  }
513
713
  };
514
714
 
@@ -577,16 +777,62 @@ Update steering docs as needed.`, 'kspec-analyse');
577
777
  },
578
778
 
579
779
  async spec(args) {
780
+ // Parse --jira flag
781
+ const jiraIndex = args.findIndex(a => a === '--jira' || a.startsWith('--jira='));
782
+ let jiraIssues = null;
783
+
784
+ if (jiraIndex !== -1) {
785
+ // Check prerequisite
786
+ requireAtlassianMcp();
787
+
788
+ if (args[jiraIndex].startsWith('--jira=')) {
789
+ jiraIssues = args[jiraIndex].split('=')[1];
790
+ args.splice(jiraIndex, 1);
791
+ } else if (args[jiraIndex + 1] && !args[jiraIndex + 1].startsWith('-')) {
792
+ jiraIssues = args[jiraIndex + 1];
793
+ args.splice(jiraIndex, 2);
794
+ } else {
795
+ die('Usage: kspec spec --jira PROJ-123,PROJ-124 "Feature Name"');
796
+ }
797
+ }
798
+
580
799
  const feature = args.join(' ');
581
- if (!feature) die('Usage: kspec spec "Feature Name"');
800
+ if (!feature && !jiraIssues) die('Usage: kspec spec "Feature Name" [--jira ISSUE-123,ISSUE-456]');
582
801
 
583
802
  const date = formatDate(config.dateFormat || 'YYYY-MM-DD');
584
- const folder = path.join(getSpecsDir(), `${date}-${slugify(feature)}`);
803
+ const featureName = feature || `jira-${jiraIssues.split(',')[0].toLowerCase()}`;
804
+ const folder = path.join(getSpecsDir(), `${date}-${slugify(featureName)}`);
585
805
  ensureDir(folder);
586
806
  setCurrentSpec(folder);
587
807
 
588
808
  log(`Spec folder: ${folder}`);
589
- await chat(`Create specification for: ${feature}
809
+
810
+ if (jiraIssues) {
811
+ // Jira-driven spec creation
812
+ log(`Pulling requirements from Jira: ${jiraIssues}`);
813
+ await chat(`Create specification from Jira issues: ${jiraIssues}
814
+
815
+ Folder: ${folder}
816
+
817
+ WORKFLOW:
818
+ 1. Use Atlassian MCP to fetch each Jira issue: ${jiraIssues}
819
+ 2. Extract from each issue:
820
+ - Summary and description
821
+ - Acceptance criteria
822
+ - Comments (for context)
823
+ - Linked issues
824
+ 3. Consolidate into unified spec.md with:
825
+ - Problem/Context (from issue descriptions)
826
+ - Requirements (from acceptance criteria)
827
+ - Source attribution: "Source: JIRA-XXX" for each requirement
828
+ - Links to original issues
829
+ 4. Create spec-lite.md (<500 words, key requirements only)
830
+ 5. Save Jira issue keys to ${folder}/jira-links.json
831
+
832
+ IMPORTANT: Include Jira links for traceability.`, 'kspec-jira');
833
+ } else {
834
+ // Standard spec creation
835
+ await chat(`Create specification for: ${feature}
590
836
 
591
837
  Folder: ${folder}
592
838
 
@@ -595,6 +841,7 @@ Folder: ${folder}
595
841
  3. IMMEDIATELY create ${folder}/spec-lite.md (concise version, <500 words)
596
842
 
597
843
  spec-lite.md is critical - it's loaded after context compression.`, 'kspec-spec');
844
+ }
598
845
  },
599
846
 
600
847
  async 'verify-spec'(args) {
@@ -612,6 +859,120 @@ Read the codebase to check implementability.
612
859
  Report: PASS/FAIL with specific issues.`, 'kspec-verify');
613
860
  },
614
861
 
862
+ async 'sync-jira'(args) {
863
+ // Check prerequisite
864
+ requireAtlassianMcp();
865
+
866
+ const folder = getOrSelectSpec();
867
+
868
+ // Parse flags
869
+ const createFlag = args.includes('--create');
870
+ const updateIndex = args.findIndex(a => a === '--update' || a.startsWith('--update='));
871
+ let updateIssue = null;
872
+
873
+ if (updateIndex !== -1) {
874
+ if (args[updateIndex].startsWith('--update=')) {
875
+ updateIssue = args[updateIndex].split('=')[1];
876
+ } else if (args[updateIndex + 1] && !args[updateIndex + 1].startsWith('-')) {
877
+ updateIssue = args[updateIndex + 1];
878
+ } else {
879
+ die('Usage: kspec sync-jira --update PROJ-123');
880
+ }
881
+ }
882
+
883
+ if (!createFlag && !updateIssue) {
884
+ // Default to create
885
+ log('No flag specified, will create new Jira issue');
886
+ }
887
+
888
+ log(`Syncing spec to Jira: ${folder}`);
889
+
890
+ if (updateIssue) {
891
+ await chat(`Update existing Jira issue with specification.
892
+
893
+ Spec folder: ${folder}
894
+ Target issue: ${updateIssue}
895
+
896
+ WORKFLOW:
897
+ 1. Read ${folder}/spec.md
898
+ 2. Use Atlassian MCP to update ${updateIssue}:
899
+ - Update description with spec content (or add as comment)
900
+ - Add label: kspec-spec
901
+ - Add comment: "Technical specification updated via kspec"
902
+ 3. Update ${folder}/jira-links.json with the issue key
903
+ 4. Update .kspec/CONTEXT.md with Jira link
904
+
905
+ Report the updated issue URL.`, 'kspec-jira');
906
+ } else {
907
+ await chat(`Create new Jira issue from specification.
908
+
909
+ Spec folder: ${folder}
910
+
911
+ WORKFLOW:
912
+ 1. Read ${folder}/spec.md and ${folder}/spec-lite.md
913
+ 2. Check ${folder}/jira-links.json for source issues to link
914
+ 3. Use Atlassian MCP to create new issue:
915
+ - Type: Task or Story (based on project settings)
916
+ - Summary: Extract from spec title
917
+ - Description: Include spec-lite.md content
918
+ - Labels: kspec-spec, technical-specification
919
+ - Link to source issues if any
920
+ 4. Add comment requesting BA/PM review
921
+ 5. Save new issue key to ${folder}/jira-links.json
922
+ 6. Update .kspec/CONTEXT.md with new Jira link
923
+
924
+ Report the created issue URL.`, 'kspec-jira');
925
+ }
926
+ },
927
+
928
+ async 'jira-subtasks'(args) {
929
+ // Check prerequisite
930
+ requireAtlassianMcp();
931
+
932
+ const folder = getOrSelectSpec();
933
+ const tasksFile = path.join(folder, 'tasks.md');
934
+
935
+ if (!fs.existsSync(tasksFile)) {
936
+ die(`No tasks.md found in ${folder}. Run 'kspec tasks' first.`);
937
+ }
938
+
939
+ // Check for parent issue
940
+ const jiraLinksFile = path.join(folder, 'jira-links.json');
941
+ let parentIssue = args[0];
942
+
943
+ if (!parentIssue && fs.existsSync(jiraLinksFile)) {
944
+ try {
945
+ const links = JSON.parse(fs.readFileSync(jiraLinksFile, 'utf8'));
946
+ parentIssue = links.specIssue || links.sourceIssues?.[0];
947
+ } catch {}
948
+ }
949
+
950
+ if (!parentIssue) {
951
+ die(`No parent issue specified. Usage: kspec jira-subtasks PROJ-123
952
+ Or run 'kspec sync-jira' first to create a spec issue.`);
953
+ }
954
+
955
+ log(`Creating Jira subtasks from: ${folder}`);
956
+
957
+ await chat(`Create Jira subtasks from tasks.md.
958
+
959
+ Spec folder: ${folder}
960
+ Parent issue: ${parentIssue}
961
+ Tasks file: ${tasksFile}
962
+
963
+ WORKFLOW:
964
+ 1. Read ${tasksFile}
965
+ 2. For each uncompleted task (- [ ]):
966
+ - Use Atlassian MCP to create subtask under ${parentIssue}
967
+ - Summary: Task description
968
+ - Description: Include any details, file paths mentioned
969
+ - Labels: kspec-task
970
+ 3. Save created subtask keys to ${folder}/jira-links.json
971
+ 4. Update .kspec/CONTEXT.md with subtask links
972
+
973
+ Report created subtasks with their URLs.`, 'kspec-jira');
974
+ },
975
+
615
976
  async tasks(args) {
616
977
  const folder = getOrSelectSpec(args.join(' '));
617
978
  log(`Generating tasks: ${folder}`);
@@ -805,19 +1166,51 @@ Output: APPROVE or REQUEST_CHANGES with specifics.`, 'kspec-review');
805
1166
  console.log(`
806
1167
  kspec Agents
807
1168
 
808
- Agent Shortcut Purpose
809
- ─────────────────────────────────────────────
1169
+ Agent Shortcut Purpose
1170
+ ─────────────────────────────────────────────────────────
810
1171
  kspec-analyse Ctrl+Shift+A Analyse codebase, update steering
811
1172
  kspec-spec Ctrl+Shift+S Create specifications
812
1173
  kspec-tasks Ctrl+Shift+T Generate tasks from spec
813
1174
  kspec-build Ctrl+Shift+B Execute tasks with TDD
814
1175
  kspec-verify Ctrl+Shift+V Verify spec/tasks/implementation
815
1176
  kspec-review Ctrl+Shift+R Code review
1177
+ kspec-jira Ctrl+Shift+J Jira integration (requires Atlassian MCP)
816
1178
 
817
1179
  Switch: /agent swap or use keyboard shortcuts
818
1180
  `);
819
1181
  },
820
1182
 
1183
+ async update() {
1184
+ console.log(`\nkspec v${pkg.version}\n`);
1185
+ console.log('Checking for updates...');
1186
+
1187
+ try {
1188
+ const https = require('https');
1189
+ const data = await new Promise((resolve, reject) => {
1190
+ const req = https.get('https://registry.npmjs.org/kspec/latest', { timeout: 5000 }, res => {
1191
+ let body = '';
1192
+ res.on('data', chunk => body += chunk);
1193
+ res.on('end', () => resolve(body));
1194
+ });
1195
+ req.on('error', reject);
1196
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
1197
+ });
1198
+
1199
+ const latest = JSON.parse(data).version;
1200
+ saveUpdateCheck();
1201
+
1202
+ if (compareVersions(pkg.version, latest) < 0) {
1203
+ console.log(`\nUpdate available: ${pkg.version} → ${latest}`);
1204
+ console.log('\nTo update, run:');
1205
+ console.log(' npm install -g kspec\n');
1206
+ } else {
1207
+ console.log(`\nYou're on the latest version!\n`);
1208
+ }
1209
+ } catch (err) {
1210
+ console.error('\nCould not check for updates. Check your internet connection.\n');
1211
+ }
1212
+ },
1213
+
821
1214
  help() {
822
1215
  console.log(`
823
1216
  kspec - Spec-driven development for Kiro CLI
@@ -841,31 +1234,46 @@ Inside kiro-cli (recommended):
841
1234
 
842
1235
  Agents read .kspec/CONTEXT.md automatically for state.
843
1236
 
1237
+ Jira Integration (requires Atlassian MCP):
1238
+ kspec spec --jira PROJ-123,PROJ-456 "Feature"
1239
+ Create spec from Jira issues
1240
+ kspec sync-jira Create/update Jira issue from spec
1241
+ kspec sync-jira --update PROJ-123
1242
+ Update existing Jira issue
1243
+ kspec jira-subtasks Create Jira subtasks from tasks.md
1244
+ kspec jira-subtasks PROJ-123
1245
+ Create subtasks under specific issue
1246
+
844
1247
  Other:
845
1248
  kspec context Refresh/view context file
846
1249
  kspec review [target] Code review
847
1250
  kspec list List all specs
848
1251
  kspec status Current status
849
1252
  kspec agents List agents
1253
+ kspec update Check for updates
850
1254
  kspec help Show this help
851
1255
 
852
1256
  Examples:
853
1257
  kspec init # First time setup
854
1258
  kspec spec "User Auth" # CLI mode
1259
+ kspec spec --jira PROJ-123 "Auth" # From Jira story
855
1260
  kiro-cli --agent kspec-spec # Direct agent mode
856
1261
  `);
857
1262
  }
858
1263
  };
859
1264
 
860
1265
  async function run(args) {
1266
+ // Check for updates (non-blocking, cached for 24h)
1267
+ checkForUpdates();
1268
+
861
1269
  // Handle standard CLI flags first
862
1270
  if (args.includes('--help') || args.includes('-h')) {
863
1271
  return commands.help();
864
1272
  }
865
-
1273
+
866
1274
  if (args.includes('--version') || args.includes('-v')) {
867
- const pkg = require('../package.json');
868
- console.log(pkg.version);
1275
+ // Show version and check for updates
1276
+ await commands.update();
869
1277
  return;
870
1278
  }
871
1279
 
@@ -879,4 +1287,4 @@ async function run(args) {
879
1287
  }
880
1288
  }
881
1289
 
882
- module.exports = { run, commands, loadConfig, detectCli, requireCli, agentTemplates, getTaskStats, refreshContext, getCurrentTask };
1290
+ module.exports = { run, commands, loadConfig, detectCli, requireCli, agentTemplates, getTaskStats, refreshContext, getCurrentTask, checkForUpdates, compareVersions, hasAtlassianMcp, getMcpConfig };