@nahisaho/shikigami 1.22.0 → 1.24.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.
@@ -21,11 +21,14 @@ tools: ['shikigami-planner', 'shikigami-deep-research', 'shikigami-consulting-fr
21
21
  > **searchツール使用前に `npx shikigami new <ProjectName>` 必須**
22
22
 
23
23
  ```
24
- 1. プロジェクト名をユーザーに質問
25
- 2. npx shikigami new <ProjectName> 実行
26
- 3. フォルダ作成確認後、ワークフロー開始
24
+ 1. プロジェクト名をユーザーに提案して確認
25
+ 2. ユーザーの承認を待つ(この時点で何も実行しない)⭐
26
+ 3. 承認後に npx shikigami new <ProjectName> 実行
27
+ 4. フォルダ作成確認後、ワークフロー開始
27
28
  ```
28
29
 
30
+ ❌ **禁止**: 「確認いただく前に作成しておきます」など承認前の実行
31
+
29
32
  ---
30
33
 
31
34
  ## ワークフロー(5フェーズ)
@@ -96,6 +96,23 @@ project_initialization:
96
96
 
97
97
  invoke_at: "workflow_start"
98
98
 
99
+ # v1.24.0: 承認待機ルール
100
+ user_confirmation:
101
+ required: true
102
+ wait_for_approval: true
103
+ prohibited_patterns:
104
+ - "確認いただく前に作成"
105
+ - "確認前に実行"
106
+ - "先に作成しておきます"
107
+ approval_keywords:
108
+ - "はい"
109
+ - "OK"
110
+ - "ok"
111
+ - "承認"
112
+ - "approve"
113
+ - "それで"
114
+ - "お願いします"
115
+
99
116
  required_actions:
100
117
  - action: "run_command"
101
118
  command: "npx shikigami new <ProjectName>"
@@ -17,12 +17,37 @@ license: MIT
17
17
 
18
18
  **リサーチ開始前に `npx shikigami new <ProjectName>` を必ず実行。**
19
19
  - ❌ 禁止: 依頼を受けてすぐWeb検索
20
- - 正解: まずプロジェクト名を質問 → CLI実行
20
+ - 禁止: プロジェクト名確認前にフォルダ作成
21
+ - ✅ 正解: まずプロジェクト名を質問 → **ユーザーの承認を待つ** → CLI実行
21
22
 
22
23
  ---
23
24
 
24
25
  ## Phase 0: プロジェクト初期化
25
26
 
27
+ ### ⚠️ 必須フロー(v1.24.0)
28
+
29
+ ```
30
+ 1. プロジェクト名を提案してユーザーに確認
31
+ 2. ユーザーが「はい」「OK」「承認」等で同意するまで待機
32
+ 3. 承認後に npx shikigami new <ProjectName> を実行
33
+ 4. フォルダ作成確認後、ワークフロー開始
34
+ ```
35
+
36
+ ❌ **禁止パターン**:
37
+ ```
38
+ 「プロジェクト名はXXXでよろしいでしょうか?」
39
+ 「確認いただく前に、プロジェクトを作成しておきます」 ← これは禁止!
40
+ ```
41
+
42
+ ✅ **正しいパターン**:
43
+ ```
44
+ 「プロジェクト名はXXXでよろしいでしょうか?」
45
+ → ユーザーの返答を待つ(何も実行しない)
46
+ → ユーザーが承認したら npx shikigami new XXX を実行
47
+ ```
48
+
49
+ ### コマンド
50
+
26
51
  ```bash
27
52
  npx shikigami new <ProjectName> # 英数字CamelCase、ハイフン禁止
28
53
  ```
package/CHANGELOG.md CHANGED
@@ -5,6 +5,79 @@ All notable changes to SHIKIGAMI will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.24.0] - 2026-01-27
9
+
10
+ ### Fixed
11
+
12
+ - **プロジェクト名確認前にフォルダが作成される問題を修正**
13
+ - 問題: 「プロジェクト名はXXXでよろしいでしょうか?」の後、承認を待たずにフォルダを作成していた
14
+ - 原因: SKILL.mdとプロンプトに「承認を待つ」指示が不足していた
15
+ - 対策: Phase 0のフローに「ユーザー承認待機」ステップを追加
16
+
17
+ ### Changed
18
+
19
+ - **shikigami-planner/SKILL.md**
20
+ - Phase 0に「必須フロー」セクションを追加
21
+ - 禁止パターンと正しいパターンを明記
22
+ - 「確認いただく前に作成しておきます」を禁止パターンとして明示
23
+
24
+ - **shikigami-full-research.prompt.md**
25
+ - 最重要ルールに「ユーザーの承認を待つ」ステップを追加
26
+ - 禁止行動を明示
27
+
28
+ - **auto-invoke-triggers.yaml**
29
+ - `user_confirmation` セクションを追加
30
+ - `wait_for_approval: true` で承認待機を強制
31
+ - `prohibited_patterns` で禁止パターンを定義
32
+ - `approval_keywords` で承認キーワードを定義
33
+
34
+ ---
35
+
36
+ ## [1.23.0] - 2026-01-27
37
+
38
+ ### Added
39
+
40
+ - **MCPツール: プロジェクトコンテキスト管理**
41
+ - `set_project`: アクティブなプロジェクトディレクトリを設定
42
+ - `projectPath`: 手動でプロジェクトパスを指定
43
+ - `autoDetect`: projects/内の最新プロジェクトを自動検出
44
+ - `get_project`: 現在のプロジェクト情報を取得
45
+
46
+ - **MCPツール: ファイル永続化機能**
47
+ - `save_prompt`: プロンプトをprompts/に保存
48
+ - タイプ: original(元プロンプト), structured(構造化後), refinement(修正)
49
+ - 自動フロントマター付与(timestamp, project_id, type)
50
+ - `save_research`: 検索結果をresearch/に保存
51
+ - ソース種別: search(Web検索), visit(ページ訪問), manual(手動)
52
+ - Markdown/JSON両形式対応
53
+ - クエリ情報をメタデータとして保存
54
+
55
+ - **新規モジュール**
56
+ - `tools/project.ts`: プロジェクト管理機能(検証、パス解決、自動検出)
57
+ - `tools/save.ts`: ファイル保存機能(プロンプト、リサーチ)
58
+
59
+ - **CLIコマンド: upgrade/update**
60
+ - `npx shikigami upgrade` または `npx shikigami update` で最新版にアップグレード
61
+ - 内部的に `npx shikigami init --force` と同じ動作
62
+ - 既存ファイルを上書きしてSHIKIGAMIを更新
63
+
64
+ ### Fixed
65
+
66
+ - **プロンプトがprompts/に保存されない問題を修正**
67
+ - 原因: MCPサーバーにファイル書き込みツールが存在しなかった
68
+ - 対策: `save_prompt`ツールを実装
69
+
70
+ - **検索結果がresearch/に保存されない問題を修正**
71
+ - 原因: searchツールは結果を返すだけで永続化機能がなかった
72
+ - 対策: `save_research`ツールを実装
73
+
74
+ ### Changed
75
+
76
+ - **index.ts**: 新規ツール定義(4ツール)とハンドラー追加
77
+ - **MCPサーバー**: ツール総数 7→11 に増加
78
+
79
+ ---
80
+
8
81
  ## [1.22.0] - 2026-01-27
9
82
 
10
83
  ### Added
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nahisaho/shikigami-mcp-server",
3
- "version": "1.16.6",
3
+ "version": "1.23.0",
4
4
  "description": "SHIKIGAMI MCP Server - Deep Research tools with search fallback mechanism",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -27,9 +27,131 @@ import {
27
27
  type SimilarityResult,
28
28
  type SearchResult as SemanticSearchResult,
29
29
  } from './tools/embedding.js';
30
+ import {
31
+ setActiveProject,
32
+ getProjectInfo,
33
+ detectLatestProject,
34
+ } from './tools/project.js';
35
+ import {
36
+ savePrompt,
37
+ saveResearch,
38
+ saveResearchJson,
39
+ type SavePromptOptions,
40
+ type SaveResearchOptions,
41
+ } from './tools/save.js';
30
42
 
31
43
  // Tool definitions
32
44
  const TOOLS: Tool[] = [
45
+ // Project Management Tools (v1.23.0)
46
+ {
47
+ name: 'set_project',
48
+ description: `アクティブなプロジェクトディレクトリを設定。
49
+ save_prompt, save_researchの前に必ず実行。
50
+ プロジェクトパスはnpx shikigami newで作成されたフォルダを指定。`,
51
+ inputSchema: {
52
+ type: 'object',
53
+ properties: {
54
+ projectPath: {
55
+ type: 'string',
56
+ description: 'プロジェクトディレクトリの絶対パスまたは相対パス(例: projects/pj00001_MyProject_20260127)',
57
+ },
58
+ autoDetect: {
59
+ type: 'boolean',
60
+ default: false,
61
+ description: 'trueの場合、projects/内の最新プロジェクトを自動検出',
62
+ },
63
+ },
64
+ required: [],
65
+ },
66
+ },
67
+ {
68
+ name: 'get_project',
69
+ description: `現在のアクティブなプロジェクト情報を取得。
70
+ プロジェクトID、パス、各ディレクトリの情報を返却。`,
71
+ inputSchema: {
72
+ type: 'object',
73
+ properties: {},
74
+ required: [],
75
+ },
76
+ },
77
+ {
78
+ name: 'save_prompt',
79
+ description: `プロンプトをプロジェクトのprompts/ディレクトリに保存。
80
+ 元のプロンプト、構造化プロンプト、修正履歴を記録。
81
+ set_projectでプロジェクトを設定してから使用。`,
82
+ inputSchema: {
83
+ type: 'object',
84
+ properties: {
85
+ content: {
86
+ type: 'string',
87
+ description: '保存するプロンプト内容',
88
+ },
89
+ type: {
90
+ type: 'string',
91
+ enum: ['original', 'structured', 'refinement'],
92
+ description: 'プロンプトの種類(original: 元のプロンプト, structured: 構造化後, refinement: 修正)',
93
+ },
94
+ filename: {
95
+ type: 'string',
96
+ description: 'ファイル名(省略時は自動生成)',
97
+ },
98
+ version: {
99
+ type: 'number',
100
+ description: '修正バージョン番号(refinementタイプの場合)',
101
+ },
102
+ metadata: {
103
+ type: 'object',
104
+ description: '追加のメタデータ',
105
+ },
106
+ },
107
+ required: ['content', 'type'],
108
+ },
109
+ },
110
+ {
111
+ name: 'save_research',
112
+ description: `検索結果や調査内容をプロジェクトのresearch/ディレクトリに保存。
113
+ Web検索結果、ページ訪問結果、手動メモを記録。
114
+ set_projectでプロジェクトを設定してから使用。`,
115
+ inputSchema: {
116
+ type: 'object',
117
+ properties: {
118
+ content: {
119
+ type: 'string',
120
+ description: '保存する調査内容(Markdown形式推奨)',
121
+ },
122
+ query: {
123
+ type: 'string',
124
+ description: '検索クエリ(ファイル名に使用)',
125
+ },
126
+ source: {
127
+ type: 'string',
128
+ enum: ['search', 'visit', 'manual'],
129
+ default: 'manual',
130
+ description: '情報ソース(search: Web検索, visit: ページ訪問, manual: 手動入力)',
131
+ },
132
+ filename: {
133
+ type: 'string',
134
+ description: 'ファイル名(省略時は自動生成)',
135
+ },
136
+ format: {
137
+ type: 'string',
138
+ enum: ['markdown', 'json'],
139
+ default: 'markdown',
140
+ description: '保存形式',
141
+ },
142
+ data: {
143
+ type: 'object',
144
+ description: 'JSON形式で保存する場合のデータ(format=jsonの場合)',
145
+ },
146
+ metadata: {
147
+ type: 'object',
148
+ description: '追加のメタデータ',
149
+ },
150
+ },
151
+ required: ['content'],
152
+ },
153
+ },
154
+ // Search & Visit Tools
33
155
  {
34
156
  name: 'search',
35
157
  description: `バッチWeb検索を実行(DuckDuckGo使用)。
@@ -244,6 +366,141 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest)
244
366
 
245
367
  try {
246
368
  switch (name) {
369
+ // Project Management Handlers (v1.23.0)
370
+ case 'set_project': {
371
+ const projectPath = args?.projectPath as string | undefined;
372
+ const autoDetect = args?.autoDetect as boolean | undefined;
373
+
374
+ let targetPath: string;
375
+
376
+ if (autoDetect) {
377
+ const detected = detectLatestProject();
378
+ if (!detected) {
379
+ return {
380
+ content: [
381
+ {
382
+ type: 'text',
383
+ text: JSON.stringify({
384
+ success: false,
385
+ error: 'No project found in projects/ directory. Create one with: npx shikigami new <ProjectName>',
386
+ }, null, 2),
387
+ },
388
+ ],
389
+ isError: true,
390
+ };
391
+ }
392
+ targetPath = detected;
393
+ } else if (projectPath) {
394
+ targetPath = projectPath;
395
+ } else {
396
+ return {
397
+ content: [
398
+ {
399
+ type: 'text',
400
+ text: JSON.stringify({
401
+ success: false,
402
+ error: 'Either projectPath or autoDetect=true is required',
403
+ }, null, 2),
404
+ },
405
+ ],
406
+ isError: true,
407
+ };
408
+ }
409
+
410
+ const project = setActiveProject(targetPath);
411
+ return {
412
+ content: [
413
+ {
414
+ type: 'text',
415
+ text: JSON.stringify({
416
+ success: true,
417
+ message: `Active project set to: ${project.projectPath}`,
418
+ project: getProjectInfo(),
419
+ }, null, 2),
420
+ },
421
+ ],
422
+ };
423
+ }
424
+
425
+ case 'get_project': {
426
+ return {
427
+ content: [
428
+ {
429
+ type: 'text',
430
+ text: JSON.stringify(getProjectInfo(), null, 2),
431
+ },
432
+ ],
433
+ };
434
+ }
435
+
436
+ case 'save_prompt': {
437
+ const content = args?.content as string;
438
+ const type = args?.type as 'original' | 'structured' | 'refinement';
439
+ const filename = args?.filename as string | undefined;
440
+ const version = args?.version as number | undefined;
441
+ const metadata = args?.metadata as Record<string, unknown> | undefined;
442
+
443
+ const result = await savePrompt(content, {
444
+ type,
445
+ filename,
446
+ version,
447
+ metadata,
448
+ });
449
+
450
+ return {
451
+ content: [
452
+ {
453
+ type: 'text',
454
+ text: JSON.stringify(result, null, 2),
455
+ },
456
+ ],
457
+ };
458
+ }
459
+
460
+ case 'save_research': {
461
+ const content = args?.content as string;
462
+ const query = args?.query as string | undefined;
463
+ const source = args?.source as 'search' | 'visit' | 'manual' | undefined;
464
+ const filename = args?.filename as string | undefined;
465
+ const format = args?.format as 'markdown' | 'json' | undefined;
466
+ const data = args?.data as unknown;
467
+ const metadata = args?.metadata as Record<string, unknown> | undefined;
468
+
469
+ if (format === 'json' && data) {
470
+ const result = await saveResearchJson(data, {
471
+ query,
472
+ source,
473
+ filename,
474
+ metadata,
475
+ });
476
+ return {
477
+ content: [
478
+ {
479
+ type: 'text',
480
+ text: JSON.stringify(result, null, 2),
481
+ },
482
+ ],
483
+ };
484
+ }
485
+
486
+ const result = await saveResearch(content, {
487
+ query,
488
+ source,
489
+ filename,
490
+ metadata,
491
+ });
492
+
493
+ return {
494
+ content: [
495
+ {
496
+ type: 'text',
497
+ text: JSON.stringify(result, null, 2),
498
+ },
499
+ ],
500
+ };
501
+ }
502
+
503
+ // Search & Visit Handlers
247
504
  case 'search': {
248
505
  const queryInput = args?.query as string | string[];
249
506
  const maxResults = (args?.maxResults as number) ?? 10;
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Project Management Module Tests
3
+ * v1.23.0 - REQ-SHIKIGAMI-016
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import {
10
+ setActiveProject,
11
+ getActiveProject,
12
+ hasActiveProject,
13
+ clearActiveProject,
14
+ getProjectSubdirectory,
15
+ getProjectInfo,
16
+ isValidProjectDirectory,
17
+ detectLatestProject,
18
+ } from '../project.js';
19
+
20
+ // Mock fs module
21
+ vi.mock('fs');
22
+
23
+ describe('Project Management Module', () => {
24
+ const mockProjectPath = '/test/projects/pj00001_TestProject_20260127';
25
+
26
+ beforeEach(() => {
27
+ clearActiveProject();
28
+ vi.resetAllMocks();
29
+ });
30
+
31
+ afterEach(() => {
32
+ clearActiveProject();
33
+ });
34
+
35
+ describe('isValidProjectDirectory', () => {
36
+ it('should return true for valid project directory', () => {
37
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
38
+ const pathStr = String(p);
39
+ return (
40
+ pathStr.includes('prompts') ||
41
+ pathStr.includes('research') ||
42
+ pathStr.includes('reports') ||
43
+ pathStr.includes('manifest.yaml')
44
+ );
45
+ });
46
+
47
+ expect(isValidProjectDirectory(mockProjectPath)).toBe(true);
48
+ });
49
+
50
+ it('should return false if prompts directory is missing', () => {
51
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
52
+ const pathStr = String(p);
53
+ return !pathStr.includes('prompts');
54
+ });
55
+
56
+ expect(isValidProjectDirectory(mockProjectPath)).toBe(false);
57
+ });
58
+
59
+ it('should return false if manifest.yaml is missing', () => {
60
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
61
+ const pathStr = String(p);
62
+ return (
63
+ pathStr.includes('prompts') ||
64
+ pathStr.includes('research') ||
65
+ pathStr.includes('reports')
66
+ );
67
+ });
68
+
69
+ expect(isValidProjectDirectory(mockProjectPath)).toBe(false);
70
+ });
71
+ });
72
+
73
+ describe('setActiveProject', () => {
74
+ it('should set active project successfully', () => {
75
+ vi.mocked(fs.existsSync).mockReturnValue(true);
76
+
77
+ const project = setActiveProject(mockProjectPath);
78
+
79
+ expect(project.projectPath).toBe(mockProjectPath);
80
+ expect(project.projectId).toBe('pj00001');
81
+ expect(project.projectName).toBe('TestProject');
82
+ expect(project.promptsDir).toBe(path.join(mockProjectPath, 'prompts'));
83
+ expect(project.researchDir).toBe(path.join(mockProjectPath, 'research'));
84
+ expect(project.reportsDir).toBe(path.join(mockProjectPath, 'reports'));
85
+ });
86
+
87
+ it('should throw error if directory does not exist', () => {
88
+ vi.mocked(fs.existsSync).mockReturnValue(false);
89
+
90
+ expect(() => setActiveProject('/nonexistent')).toThrow(
91
+ 'Project directory not found'
92
+ );
93
+ });
94
+
95
+ it('should throw error if directory is invalid', () => {
96
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
97
+ const pathStr = String(p);
98
+ // Path exists but subdirectories don't
99
+ return pathStr === mockProjectPath;
100
+ });
101
+
102
+ expect(() => setActiveProject(mockProjectPath)).toThrow(
103
+ 'Invalid project directory'
104
+ );
105
+ });
106
+ });
107
+
108
+ describe('getActiveProject', () => {
109
+ it('should return null when no project is active', () => {
110
+ expect(getActiveProject()).toBeNull();
111
+ });
112
+
113
+ it('should return active project when set', () => {
114
+ vi.mocked(fs.existsSync).mockReturnValue(true);
115
+ setActiveProject(mockProjectPath);
116
+
117
+ const project = getActiveProject();
118
+ expect(project).not.toBeNull();
119
+ expect(project?.projectId).toBe('pj00001');
120
+ });
121
+ });
122
+
123
+ describe('hasActiveProject', () => {
124
+ it('should return false when no project is active', () => {
125
+ expect(hasActiveProject()).toBe(false);
126
+ });
127
+
128
+ it('should return true when project is active', () => {
129
+ vi.mocked(fs.existsSync).mockReturnValue(true);
130
+ setActiveProject(mockProjectPath);
131
+
132
+ expect(hasActiveProject()).toBe(true);
133
+ });
134
+ });
135
+
136
+ describe('clearActiveProject', () => {
137
+ it('should clear active project', () => {
138
+ vi.mocked(fs.existsSync).mockReturnValue(true);
139
+ setActiveProject(mockProjectPath);
140
+ expect(hasActiveProject()).toBe(true);
141
+
142
+ clearActiveProject();
143
+ expect(hasActiveProject()).toBe(false);
144
+ });
145
+ });
146
+
147
+ describe('getProjectSubdirectory', () => {
148
+ it('should throw error when no project is active', () => {
149
+ expect(() => getProjectSubdirectory('prompts')).toThrow(
150
+ 'No active project'
151
+ );
152
+ });
153
+
154
+ it('should return correct subdirectory path', () => {
155
+ vi.mocked(fs.existsSync).mockReturnValue(true);
156
+ setActiveProject(mockProjectPath);
157
+
158
+ expect(getProjectSubdirectory('prompts')).toBe(
159
+ path.join(mockProjectPath, 'prompts')
160
+ );
161
+ expect(getProjectSubdirectory('research')).toBe(
162
+ path.join(mockProjectPath, 'research')
163
+ );
164
+ expect(getProjectSubdirectory('reports')).toBe(
165
+ path.join(mockProjectPath, 'reports')
166
+ );
167
+ });
168
+ });
169
+
170
+ describe('getProjectInfo', () => {
171
+ it('should return inactive status when no project is active', () => {
172
+ const info = getProjectInfo();
173
+ expect(info.active).toBe(false);
174
+ expect(info.message).toContain('No active project');
175
+ });
176
+
177
+ it('should return full project info when active', () => {
178
+ vi.mocked(fs.existsSync).mockReturnValue(true);
179
+ setActiveProject(mockProjectPath);
180
+
181
+ const info = getProjectInfo();
182
+ expect(info.active).toBe(true);
183
+ expect(info.projectId).toBe('pj00001');
184
+ expect(info.projectName).toBe('TestProject');
185
+ expect(info.directories).toBeDefined();
186
+ });
187
+ });
188
+
189
+ describe('detectLatestProject', () => {
190
+ it('should return null if projects directory does not exist', () => {
191
+ vi.mocked(fs.existsSync).mockReturnValue(false);
192
+
193
+ expect(detectLatestProject('/test')).toBeNull();
194
+ });
195
+
196
+ it('should return latest project path', () => {
197
+ vi.mocked(fs.existsSync).mockReturnValue(true);
198
+ vi.mocked(fs.readdirSync).mockReturnValue([
199
+ { name: 'pj00001_Old_20260101', isDirectory: () => true },
200
+ { name: 'pj00002_New_20260127', isDirectory: () => true },
201
+ { name: '.hidden', isDirectory: () => true },
202
+ ] as unknown as fs.Dirent[]);
203
+ vi.mocked(fs.statSync).mockImplementation((p) => {
204
+ const pathStr = String(p);
205
+ if (pathStr.includes('pj00002')) {
206
+ return { mtime: new Date('2026-01-27') } as fs.Stats;
207
+ }
208
+ return { mtime: new Date('2026-01-01') } as fs.Stats;
209
+ });
210
+
211
+ const result = detectLatestProject('/test');
212
+ expect(result).toContain('pj00002_New_20260127');
213
+ });
214
+ });
215
+ });