@make-u-free/migi 0.1.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/.env.example +1 -0
- package/README.md +217 -0
- package/bin/migi.js +113 -0
- package/package.json +24 -0
- package/skills/secretary.md +41 -0
- package/src/agent.js +94 -0
- package/src/context.js +73 -0
- package/src/onboarding.js +123 -0
- package/src/permissions.js +22 -0
- package/src/setup.js +111 -0
- package/src/skills.js +41 -0
- package/src/tools.js +151 -0
- package/templates/company-migi.md +67 -0
- package/templates/secretary-migi.md +30 -0
package/.env.example
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
OPENAI_API_KEY=sk-...
|
package/README.md
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Migi(ミギ)
|
|
2
|
+
|
|
3
|
+
> あなたの右腕として動く AI エージェント CLI
|
|
4
|
+
> Powered by OpenAI API — by MAKE U FREE
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 全体アーキテクチャ
|
|
9
|
+
|
|
10
|
+
```mermaid
|
|
11
|
+
graph TD
|
|
12
|
+
subgraph bin
|
|
13
|
+
B[migi.js エントリーポイント]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
subgraph src
|
|
17
|
+
S[setup.js セットアップ]
|
|
18
|
+
O[onboarding.js ワークスペース初期化]
|
|
19
|
+
C[context.js コンテキスト読み込み]
|
|
20
|
+
SK[skills.js スキルルーティング]
|
|
21
|
+
A[agent.js 会話ループ]
|
|
22
|
+
T[tools.js ファイル操作・コマンド実行]
|
|
23
|
+
P[permissions.js 許可制]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
subgraph fs
|
|
27
|
+
CFG[config.json APIキー・名前・モデル]
|
|
28
|
+
MEM[memory.md グローバルメモリ]
|
|
29
|
+
MIGIMD[MIGI.md ワークスペース設定]
|
|
30
|
+
SKILLS[skills/ スキルファイル]
|
|
31
|
+
TPL[templates/ 初期化テンプレート]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
OAI[OpenAI API]
|
|
35
|
+
|
|
36
|
+
B --> S
|
|
37
|
+
B --> O
|
|
38
|
+
B --> C
|
|
39
|
+
B --> SK
|
|
40
|
+
B --> A
|
|
41
|
+
S --> CFG
|
|
42
|
+
O --> TPL
|
|
43
|
+
O --> MIGIMD
|
|
44
|
+
C --> MEM
|
|
45
|
+
C --> MIGIMD
|
|
46
|
+
SK --> SKILLS
|
|
47
|
+
A --> OAI
|
|
48
|
+
A --> T
|
|
49
|
+
A --> P
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 起動フロー
|
|
55
|
+
|
|
56
|
+
```mermaid
|
|
57
|
+
graph TD
|
|
58
|
+
Start([migi 起動]) --> CheckCfg{config.json があるか}
|
|
59
|
+
|
|
60
|
+
CheckCfg -->|なし| InputKey[APIキー入力]
|
|
61
|
+
InputKey --> SelectModel[モデル選択]
|
|
62
|
+
SelectModel --> InputName[名前入力]
|
|
63
|
+
InputName --> AIName[AIが名前を解釈]
|
|
64
|
+
AIName --> SaveCfg[config.json に保存]
|
|
65
|
+
SaveCfg --> CheckWS
|
|
66
|
+
|
|
67
|
+
CheckCfg -->|あり| LoadCfg[設定読み込み]
|
|
68
|
+
LoadCfg --> CheckWS
|
|
69
|
+
|
|
70
|
+
CheckWS{MIGI.md または .company があるか}
|
|
71
|
+
CheckWS -->|なし| Q1[Q1 事業・活動を教えて]
|
|
72
|
+
Q1 --> Q2[Q2 目標・困りごとを教えて]
|
|
73
|
+
Q2 --> GenFiles[.company/ を生成]
|
|
74
|
+
GenFiles --> LoadCtx
|
|
75
|
+
|
|
76
|
+
CheckWS -->|あり| LoadCtx[コンテキスト読み込み]
|
|
77
|
+
LoadCtx --> Ready([メインループ開始])
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## リクエスト処理フロー
|
|
83
|
+
|
|
84
|
+
```mermaid
|
|
85
|
+
sequenceDiagram
|
|
86
|
+
actor User
|
|
87
|
+
participant CLI as migi.js
|
|
88
|
+
participant Agent as agent.js
|
|
89
|
+
participant OpenAI
|
|
90
|
+
participant Tools as tools.js
|
|
91
|
+
participant FS as ファイルシステム
|
|
92
|
+
|
|
93
|
+
User->>CLI: テキスト入力
|
|
94
|
+
|
|
95
|
+
alt スキルコマンド
|
|
96
|
+
CLI->>CLI: スキルファイルを検索
|
|
97
|
+
CLI->>Agent: スキル内容+入力を展開して送信
|
|
98
|
+
else 通常テキスト
|
|
99
|
+
CLI->>Agent: そのまま送信
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
Agent->>OpenAI: messages+tool定義を送信
|
|
103
|
+
|
|
104
|
+
loop ツール呼び出しがある間
|
|
105
|
+
OpenAI-->>Agent: tool_calls レスポンス
|
|
106
|
+
|
|
107
|
+
alt 読み取り系 自動承認
|
|
108
|
+
Agent->>Tools: 即実行
|
|
109
|
+
else 書き込み・実行系 要確認
|
|
110
|
+
Agent->>User: 実行しますか y/N
|
|
111
|
+
User->>Agent: 承認 or 拒否
|
|
112
|
+
Agent->>Tools: 承認なら実行
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
Tools->>FS: ファイル操作 or コマンド実行
|
|
116
|
+
FS-->>Tools: 結果
|
|
117
|
+
Tools-->>Agent: 結果を返す
|
|
118
|
+
Agent->>OpenAI: tool results を送信
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
OpenAI-->>Agent: 最終回答
|
|
122
|
+
Agent-->>User: 返答を表示
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## コンテキスト読み込みの優先順位
|
|
128
|
+
|
|
129
|
+
```mermaid
|
|
130
|
+
graph LR
|
|
131
|
+
A[memory.md グローバル] --> B[memory.md ワークスペース]
|
|
132
|
+
B --> C[MIGI.md ルート]
|
|
133
|
+
C --> D[.company/MIGI.md]
|
|
134
|
+
D -->|なければ| E[.company/CLAUDE.md]
|
|
135
|
+
D --> F[secretary/MIGI.md]
|
|
136
|
+
F --> G[その他部署/MIGI.md]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## スキルシステム
|
|
142
|
+
|
|
143
|
+
```mermaid
|
|
144
|
+
graph TD
|
|
145
|
+
Input[入力 /secretary など] --> Parse[コマンド名を抽出]
|
|
146
|
+
Parse --> Search1[.migi/skills/ を探す ユーザー定義]
|
|
147
|
+
Search1 -->|あり| Load[スキルファイルを読み込む]
|
|
148
|
+
Search1 -->|なし| Search2[skills/ を探す ビルトイン]
|
|
149
|
+
Search2 -->|あり| Load
|
|
150
|
+
Search2 -->|なし| NotFound[スキルが見つかりません]
|
|
151
|
+
Load --> Expand[スキル内容+引数を展開]
|
|
152
|
+
Expand --> Agent[エージェントに送信]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## ファイル構成
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
migi/
|
|
161
|
+
├── bin/
|
|
162
|
+
│ └── migi.js # エントリーポイント・メインループ
|
|
163
|
+
├── src/
|
|
164
|
+
│ ├── agent.js # OpenAI 会話ループ・ツール呼び出し制御
|
|
165
|
+
│ ├── context.js # MIGI.md / memory.md の読み込み
|
|
166
|
+
│ ├── onboarding.js # 空ワークスペースの初期化ウィザード
|
|
167
|
+
│ ├── permissions.js # 書き込み・実行の許可制
|
|
168
|
+
│ ├── setup.js # APIキー・モデル・名前のセットアップ
|
|
169
|
+
│ ├── skills.js # /コマンドのスキルルーティング
|
|
170
|
+
│ └── tools.js # ファイル操作・コマンド実行ツール
|
|
171
|
+
├── skills/
|
|
172
|
+
│ └── secretary.md # ビルトインスキル(秘書モード)
|
|
173
|
+
├── templates/
|
|
174
|
+
│ ├── company-migi.md # .company/MIGI.md のテンプレート
|
|
175
|
+
│ └── secretary-migi.md # secretary/MIGI.md のテンプレート
|
|
176
|
+
└── package.json
|
|
177
|
+
|
|
178
|
+
# ユーザーのワークスペース
|
|
179
|
+
{cwd}/
|
|
180
|
+
├── MIGI.md # ワークスペース設定(なければ CLAUDE.md)
|
|
181
|
+
├── todos/
|
|
182
|
+
│ └── YYYY-MM-DD.md
|
|
183
|
+
└── .company/
|
|
184
|
+
├── MIGI.md
|
|
185
|
+
└── secretary/
|
|
186
|
+
├── MIGI.md
|
|
187
|
+
├── inbox/
|
|
188
|
+
└── notes/
|
|
189
|
+
|
|
190
|
+
# グローバル設定
|
|
191
|
+
~/.migi/
|
|
192
|
+
├── config.json # APIキー・モデル・名前
|
|
193
|
+
└── memory.md # 全ワークスペース共通メモリ
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## セットアップ
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
git clone https://github.com/kazuhiro-yourself/migi.git
|
|
202
|
+
cd migi
|
|
203
|
+
npm install
|
|
204
|
+
|
|
205
|
+
# 作業ディレクトリで起動(初回は自動セットアップ)
|
|
206
|
+
cd ~/your-workspace
|
|
207
|
+
node /path/to/migi/bin/migi.js
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## 使い方
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
> 今日のTODO見せて # 普通に話しかけるだけ
|
|
214
|
+
> /secretary # 秘書モードを明示的に起動
|
|
215
|
+
> /config # 設定変更
|
|
216
|
+
> /exit # 終了
|
|
217
|
+
```
|
package/bin/migi.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import readline from 'readline'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import dotenv from 'dotenv'
|
|
5
|
+
import { MigiAgent } from '../src/agent.js'
|
|
6
|
+
import { loadContext } from '../src/context.js'
|
|
7
|
+
import { loadGlobalConfig, runSetup } from '../src/setup.js'
|
|
8
|
+
import { resolveSkill, parseSkillInput, expandSkill } from '../src/skills.js'
|
|
9
|
+
import { isEmptyWorkspace, runOnboarding } from '../src/onboarding.js'
|
|
10
|
+
|
|
11
|
+
dotenv.config()
|
|
12
|
+
|
|
13
|
+
// ---- readline を最初に作る(全ての対話で共用) ----
|
|
14
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
15
|
+
const promptFn = (q) => new Promise((resolve) => rl.question(q, resolve))
|
|
16
|
+
|
|
17
|
+
// ---- APIキー・設定の解決(優先度: 環境変数 > グローバル設定 > セットアップ) ----
|
|
18
|
+
let apiKey = process.env.OPENAI_API_KEY
|
|
19
|
+
let model = 'gpt-4o'
|
|
20
|
+
let agentName = 'Migi'
|
|
21
|
+
|
|
22
|
+
if (!apiKey) {
|
|
23
|
+
const config = loadGlobalConfig()
|
|
24
|
+
if (config?.openai_api_key) {
|
|
25
|
+
apiKey = config.openai_api_key
|
|
26
|
+
model = config.model || 'gpt-4o'
|
|
27
|
+
agentName = config.name || 'Migi'
|
|
28
|
+
} else {
|
|
29
|
+
const config = await runSetup(promptFn)
|
|
30
|
+
apiKey = config.openai_api_key
|
|
31
|
+
model = config.model || 'gpt-4o'
|
|
32
|
+
agentName = config.name || 'Migi'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---- 空ワークスペース検出 → オンボーディング ----
|
|
37
|
+
const cwd = process.cwd()
|
|
38
|
+
if (isEmptyWorkspace(cwd)) {
|
|
39
|
+
const proceed = await promptFn(
|
|
40
|
+
chalk.cyan('\n このフォルダにはまだ設定がありません。セットアップしますか? [Y/n] ')
|
|
41
|
+
)
|
|
42
|
+
if (proceed.trim().toLowerCase() !== 'n') {
|
|
43
|
+
await runOnboarding(cwd, promptFn)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---- コンテキスト読み込み ----
|
|
48
|
+
const { context, loaded } = await loadContext(cwd)
|
|
49
|
+
|
|
50
|
+
// ---- 起動メッセージ ----
|
|
51
|
+
console.log(chalk.bold.cyan(`\n ${agentName} — by MAKE U FREE`))
|
|
52
|
+
console.log(chalk.gray(` モデル: ${model}`))
|
|
53
|
+
if (loaded.length > 0) {
|
|
54
|
+
for (const l of loaded) console.log(chalk.dim(` ✓ ${l}`))
|
|
55
|
+
}
|
|
56
|
+
console.log(chalk.dim('\n /secretary 秘書モード'))
|
|
57
|
+
console.log(chalk.dim(' /config 設定変更'))
|
|
58
|
+
console.log(chalk.dim(' /exit 終了\n'))
|
|
59
|
+
|
|
60
|
+
const agent = new MigiAgent({ context, promptFn, apiKey, model, name: agentName })
|
|
61
|
+
|
|
62
|
+
// ---- メインループ ----
|
|
63
|
+
function prompt() {
|
|
64
|
+
rl.question(chalk.cyan('> '), async (line) => {
|
|
65
|
+
const input = line.trim()
|
|
66
|
+
if (!input) return prompt()
|
|
67
|
+
|
|
68
|
+
// --- ビルトインコマンド ---
|
|
69
|
+
if (input === '/exit' || input === '/quit') {
|
|
70
|
+
console.log(chalk.cyan(`\n お疲れ様でした!またね。\n`))
|
|
71
|
+
process.exit(0)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (input === '/config') {
|
|
75
|
+
await runSetup(promptFn)
|
|
76
|
+
console.log(chalk.yellow(' 再起動して設定を反映してください。\n'))
|
|
77
|
+
return prompt()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- スキルルーティング ---
|
|
81
|
+
const parsed = parseSkillInput(input)
|
|
82
|
+
if (parsed) {
|
|
83
|
+
const skill = resolveSkill(parsed.name, process.cwd())
|
|
84
|
+
if (skill) {
|
|
85
|
+
console.log(chalk.dim(` [スキル: ${parsed.name}]\n`))
|
|
86
|
+
const expanded = expandSkill(skill.content, parsed.args)
|
|
87
|
+
try {
|
|
88
|
+
const reply = await agent.chat(expanded)
|
|
89
|
+
console.log('\n' + reply + '\n')
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error(chalk.red('\n エラー: ' + err.message + '\n'))
|
|
92
|
+
}
|
|
93
|
+
return prompt()
|
|
94
|
+
} else {
|
|
95
|
+
console.log(chalk.yellow(` スキル「${parsed.name}」が見つかりません。`))
|
|
96
|
+
console.log(chalk.dim(` .migi/skills/${parsed.name}.md を作成してください。\n`))
|
|
97
|
+
return prompt()
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// --- 通常チャット ---
|
|
102
|
+
try {
|
|
103
|
+
const reply = await agent.chat(input)
|
|
104
|
+
console.log('\n' + reply + '\n')
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.error(chalk.red('\n エラー: ' + err.message + '\n'))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
prompt()
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
prompt()
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@make-u-free/migi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Your AI right-hand agent. Works anywhere, with any LLM API.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"migi": "./bin/migi.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node bin/migi.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"chalk": "^5.3.0",
|
|
14
|
+
"dotenv": "^16.4.0",
|
|
15
|
+
"glob": "^11.0.0",
|
|
16
|
+
"openai": "^4.0.0"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18.0.0"
|
|
20
|
+
},
|
|
21
|
+
"keywords": ["ai", "agent", "cli", "openai", "secretary"],
|
|
22
|
+
"author": "MAKE U FREE",
|
|
23
|
+
"license": "MIT"
|
|
24
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# 秘書モード
|
|
2
|
+
|
|
3
|
+
あなたは今、秘書として動作します。
|
|
4
|
+
ロードされたコンテキスト(.company/CLAUDE.md・secretary/CLAUDE.md)のルールに従ってください。
|
|
5
|
+
|
|
6
|
+
## 基本動作
|
|
7
|
+
|
|
8
|
+
- 秘書が常に窓口。ユーザーは部署を意識しなくていい
|
|
9
|
+
- 秘書で完結するもの(TODO・メモ・壁打ち・雑談)は直接対応
|
|
10
|
+
- 部署が必要な場合は、該当部署のフォルダに直接書き込む
|
|
11
|
+
|
|
12
|
+
## TODOを扱うとき
|
|
13
|
+
|
|
14
|
+
- 日次TODOは `{cwd}/todos/YYYY-MM-DD.md`(.company の外)
|
|
15
|
+
- 形式: `- [ ] タスク内容 | 優先度: 高/通常/低 | 期限: YYYY-MM-DD`
|
|
16
|
+
- 同日ファイルが存在する場合は追記。新規作成しない
|
|
17
|
+
|
|
18
|
+
## メモ・アイデアを扱うとき
|
|
19
|
+
|
|
20
|
+
- 迷ったら `.company/secretary/inbox/YYYY-MM-DD.md` に入れる
|
|
21
|
+
- 追記時はタイムスタンプを付ける
|
|
22
|
+
- 壁打ちの結論が出たら `notes/` への保存を提案する
|
|
23
|
+
|
|
24
|
+
## 意思決定・学びを扱うとき
|
|
25
|
+
|
|
26
|
+
- 意思決定 → `.company/secretary/notes/YYYY-MM-DD-decisions.md`
|
|
27
|
+
- 学び → `.company/secretary/notes/YYYY-MM-DD-learnings.md`
|
|
28
|
+
- 言われなくても重要なものは自動記録する
|
|
29
|
+
|
|
30
|
+
## ダッシュボードを求められたとき
|
|
31
|
+
|
|
32
|
+
今日のTODOファイルを読んで、以下の形式で表示する:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
36
|
+
ダッシュボード - YYYY-MM-DD
|
|
37
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
38
|
+
TODO: X件未完了 / Y件完了
|
|
39
|
+
[未完了タスクの一覧]
|
|
40
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
41
|
+
```
|
package/src/agent.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import OpenAI from 'openai'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { toolSchemas, executeTool } from './tools.js'
|
|
4
|
+
import { createPermissionChecker } from './permissions.js'
|
|
5
|
+
|
|
6
|
+
export class MigiAgent {
|
|
7
|
+
constructor({ context = '', promptFn = null, apiKey = null, model = 'gpt-4o', name = 'Migi' } = {}) {
|
|
8
|
+
this.client = new OpenAI({ apiKey: apiKey || process.env.OPENAI_API_KEY })
|
|
9
|
+
this.model = model
|
|
10
|
+
this.history = []
|
|
11
|
+
this.checkPermission = createPermissionChecker(promptFn || (() => Promise.resolve('y')))
|
|
12
|
+
|
|
13
|
+
const cwd = process.cwd()
|
|
14
|
+
const BASE_SYSTEM_PROMPT = `\
|
|
15
|
+
あなたの名前は「${name}」です。ユーザーがつけてくれた名前です。
|
|
16
|
+
自己紹介や会話の中で、この名前を自分の名前として使ってください。
|
|
17
|
+
|
|
18
|
+
あなたはユーザーの右腕として動くAIエージェントです。
|
|
19
|
+
仕事も人生も、何でも一緒に動きます。
|
|
20
|
+
ファイルの読み書き・コマンド実行・情報整理・壁打ち・タスク管理、何でもこなします。
|
|
21
|
+
|
|
22
|
+
## 口調
|
|
23
|
+
- 丁寧だが堅すぎない。「〜ですね!」「承知しました」「いいですね!」
|
|
24
|
+
- 主体的に提案する。「ついでにこれもやっておきましょうか?」
|
|
25
|
+
- 壁打ちのときはカジュアルに寄り添う
|
|
26
|
+
|
|
27
|
+
## 環境
|
|
28
|
+
- 今日の日付: ${new Date().toISOString().split('T')[0]}
|
|
29
|
+
- カレントディレクトリ: ${cwd}
|
|
30
|
+
- ファイルパスは必ずこのディレクトリを基準に構築すること
|
|
31
|
+
- 相対パスは使わず、常に絶対パスでツールを呼び出すこと
|
|
32
|
+
`
|
|
33
|
+
this.systemPrompt = BASE_SYSTEM_PROMPT +
|
|
34
|
+
(context ? `\n## ロードされたコンテキスト\n${context}` : '')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async chat(userMessage) {
|
|
38
|
+
this.history.push({ role: 'user', content: userMessage })
|
|
39
|
+
|
|
40
|
+
const messages = [
|
|
41
|
+
{ role: 'system', content: this.systemPrompt },
|
|
42
|
+
...this.history
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
while (true) {
|
|
46
|
+
const response = await this.client.chat.completions.create({
|
|
47
|
+
model: this.model,
|
|
48
|
+
messages,
|
|
49
|
+
tools: toolSchemas,
|
|
50
|
+
tool_choice: 'auto'
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const choice = response.choices[0]
|
|
54
|
+
messages.push(choice.message)
|
|
55
|
+
this.history.push(choice.message)
|
|
56
|
+
|
|
57
|
+
// 通常の返答
|
|
58
|
+
if (choice.finish_reason === 'stop') {
|
|
59
|
+
return choice.message.content
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ツール呼び出し
|
|
63
|
+
if (choice.finish_reason === 'tool_calls') {
|
|
64
|
+
const toolResults = []
|
|
65
|
+
|
|
66
|
+
for (const toolCall of choice.message.tool_calls) {
|
|
67
|
+
const args = JSON.parse(toolCall.function.arguments)
|
|
68
|
+
const name = toolCall.function.name
|
|
69
|
+
|
|
70
|
+
console.log(chalk.dim(`\n [${name}]`))
|
|
71
|
+
|
|
72
|
+
const approved = await this.checkPermission(name, args)
|
|
73
|
+
let result
|
|
74
|
+
|
|
75
|
+
if (approved) {
|
|
76
|
+
result = await executeTool(name, args)
|
|
77
|
+
} else {
|
|
78
|
+
result = 'ユーザーによりキャンセルされました'
|
|
79
|
+
console.log(chalk.dim(' → キャンセル'))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
toolResults.push({
|
|
83
|
+
role: 'tool',
|
|
84
|
+
tool_call_id: toolCall.id,
|
|
85
|
+
content: String(result)
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
messages.push(...toolResults)
|
|
90
|
+
this.history.push(...toolResults)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
package/src/context.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs'
|
|
2
|
+
import { join, dirname } from 'path'
|
|
3
|
+
import { homedir } from 'os'
|
|
4
|
+
import { glob } from 'glob'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* カレントディレクトリから関連するコンテキストファイルをすべて読み込む
|
|
8
|
+
* 優先度: グローバルメモリ → ワークスペースメモリ → CLAUDE.md → .company/**\/CLAUDE.md
|
|
9
|
+
*/
|
|
10
|
+
export async function loadContext(cwd = process.cwd()) {
|
|
11
|
+
const parts = []
|
|
12
|
+
const loaded = []
|
|
13
|
+
|
|
14
|
+
// MIGI.md を優先、なければ CLAUDE.md にフォールバック
|
|
15
|
+
const loadWithFallback = (labelPrefix, dir, filename) => {
|
|
16
|
+
const migiPath = join(dir, 'MIGI.md')
|
|
17
|
+
const claudePath = join(dir, filename)
|
|
18
|
+
if (existsSync(migiPath)) {
|
|
19
|
+
parts.push(`### ${labelPrefix}MIGI.md\n${readFileSync(migiPath, 'utf-8')}`)
|
|
20
|
+
loaded.push(`${labelPrefix}MIGI.md`)
|
|
21
|
+
} else if (existsSync(claudePath)) {
|
|
22
|
+
parts.push(`### ${labelPrefix}${filename}\n${readFileSync(claudePath, 'utf-8')}`)
|
|
23
|
+
loaded.push(`${labelPrefix}${filename}`)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const load = (label, path) => {
|
|
28
|
+
if (existsSync(path)) {
|
|
29
|
+
parts.push(`### ${label}\n${readFileSync(path, 'utf-8')}`)
|
|
30
|
+
loaded.push(label)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 1. グローバルメモリ (~/.migi/memory.md)
|
|
35
|
+
load('グローバルメモリ', join(homedir(), '.migi', 'memory.md'))
|
|
36
|
+
|
|
37
|
+
// 2. ワークスペースメモリ (.migi/memory.md)
|
|
38
|
+
load('ワークスペースメモリ', join(cwd, '.migi', 'memory.md'))
|
|
39
|
+
|
|
40
|
+
// 3. ルートの MIGI.md → CLAUDE.md
|
|
41
|
+
loadWithFallback('', cwd, 'CLAUDE.md')
|
|
42
|
+
|
|
43
|
+
// 4. .company/ 以下(MIGI.md 優先、なければ CLAUDE.md)
|
|
44
|
+
const companyDir = join(cwd, '.company')
|
|
45
|
+
if (existsSync(companyDir)) {
|
|
46
|
+
const migiFiles = await glob('**/MIGI.md', { cwd: companyDir })
|
|
47
|
+
const claudeFiles = await glob('**/CLAUDE.md', { cwd: companyDir })
|
|
48
|
+
|
|
49
|
+
// MIGI.md があるディレクトリは MIGI.md を使い、CLAUDE.md はスキップ
|
|
50
|
+
const migiDirs = new Set(migiFiles.map(f => dirname(f)))
|
|
51
|
+
const allFiles = [
|
|
52
|
+
...migiFiles,
|
|
53
|
+
...claudeFiles.filter(f => !migiDirs.has(dirname(f)))
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
// ルート → secretary → その他 の順
|
|
57
|
+
allFiles.sort((a, b) => {
|
|
58
|
+
const aBase = a.replace(/\/(MIGI|CLAUDE)\.md$/, '').replace(/^(MIGI|CLAUDE)\.md$/, '')
|
|
59
|
+
const bBase = b.replace(/\/(MIGI|CLAUDE)\.md$/, '').replace(/^(MIGI|CLAUDE)\.md$/, '')
|
|
60
|
+
if (!aBase) return -1
|
|
61
|
+
if (!bBase) return 1
|
|
62
|
+
if (aBase.startsWith('secretary')) return -1
|
|
63
|
+
if (bBase.startsWith('secretary')) return 1
|
|
64
|
+
return aBase.localeCompare(bBase)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
for (const file of allFiles) {
|
|
68
|
+
load(`.company/${file}`, join(companyDir, file))
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { context: parts.join('\n\n---\n\n'), loaded }
|
|
73
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'
|
|
2
|
+
import { join, dirname } from 'path'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
|
|
6
|
+
const PACKAGE_DIR = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ワークスペースが未初期化かどうかを判定する
|
|
10
|
+
*/
|
|
11
|
+
export function isEmptyWorkspace(cwd = process.cwd()) {
|
|
12
|
+
return (
|
|
13
|
+
!existsSync(join(cwd, 'MIGI.md')) &&
|
|
14
|
+
!existsSync(join(cwd, 'CLAUDE.md')) &&
|
|
15
|
+
!existsSync(join(cwd, '.company'))
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 対話型オンボーディングを実行してワークスペースを初期化する
|
|
21
|
+
*/
|
|
22
|
+
export async function runOnboarding(cwd, promptFn) {
|
|
23
|
+
console.log(chalk.bold.cyan('\n はじめまして!Migi があなたの右腕になります。'))
|
|
24
|
+
console.log(chalk.dim(' まず、2つだけ教えてください。\n'))
|
|
25
|
+
|
|
26
|
+
// Q1
|
|
27
|
+
console.log(chalk.dim(' あなたの事業・活動を教えてください。'))
|
|
28
|
+
console.log(chalk.dim(' 例: 個人開発、フリーランス、副業、スタートアップ、本業+副業 など\n'))
|
|
29
|
+
const businessType = await promptFn(chalk.white(' [1/2] > '))
|
|
30
|
+
|
|
31
|
+
if (!businessType.trim()) {
|
|
32
|
+
console.log(chalk.yellow('\n 入力がありませんでした。あとで /setup で再実行できます。\n'))
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Q2
|
|
37
|
+
console.log(chalk.dim('\n 今の目標や、困っていることを教えてください。'))
|
|
38
|
+
console.log(chalk.dim(' 例: 月10万目指してる、タスクが散らかる、アイデアを忘れる\n'))
|
|
39
|
+
const goals = await promptFn(chalk.white(' [2/2] > '))
|
|
40
|
+
|
|
41
|
+
console.log(chalk.dim('\n ── セットアップ中...\n'))
|
|
42
|
+
|
|
43
|
+
// ファイル生成
|
|
44
|
+
await generateWorkspace(cwd, businessType.trim(), goals.trim())
|
|
45
|
+
|
|
46
|
+
// 完了メッセージ
|
|
47
|
+
console.log(chalk.green(' セットアップ完了!\n'))
|
|
48
|
+
console.log(chalk.dim(' .company/'))
|
|
49
|
+
console.log(chalk.dim(' ├── MIGI.md'))
|
|
50
|
+
console.log(chalk.dim(' └── secretary/'))
|
|
51
|
+
console.log(chalk.dim(' ├── MIGI.md'))
|
|
52
|
+
console.log(chalk.dim(' ├── inbox/'))
|
|
53
|
+
console.log(chalk.dim(' └── notes/'))
|
|
54
|
+
console.log(chalk.dim(`\n todos/${today()}.md\n`))
|
|
55
|
+
console.log(chalk.cyan(' 何でも話しかけてください!\n'))
|
|
56
|
+
console.log(chalk.dim(' ─────────────────────────────────\n'))
|
|
57
|
+
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---- ワークスペース生成 ----
|
|
62
|
+
|
|
63
|
+
async function generateWorkspace(cwd, businessType, goals) {
|
|
64
|
+
const date = today()
|
|
65
|
+
|
|
66
|
+
// .company/MIGI.md
|
|
67
|
+
const companyDir = join(cwd, '.company')
|
|
68
|
+
mkdirSync(companyDir, { recursive: true })
|
|
69
|
+
const companyMigi = buildCompanyMigi(businessType, goals, date)
|
|
70
|
+
write(join(companyDir, 'MIGI.md'), companyMigi, '.company/MIGI.md')
|
|
71
|
+
|
|
72
|
+
// .company/secretary/
|
|
73
|
+
const secretaryDir = join(companyDir, 'secretary')
|
|
74
|
+
mkdirSync(join(secretaryDir, 'inbox'), { recursive: true })
|
|
75
|
+
mkdirSync(join(secretaryDir, 'notes'), { recursive: true })
|
|
76
|
+
|
|
77
|
+
const secretaryMigi = readFileSync(join(PACKAGE_DIR, 'templates', 'secretary-migi.md'), 'utf-8')
|
|
78
|
+
write(join(secretaryDir, 'MIGI.md'), secretaryMigi, '.company/secretary/MIGI.md')
|
|
79
|
+
|
|
80
|
+
// todos/YYYY-MM-DD.md(.company の外)
|
|
81
|
+
const todosDir = join(cwd, 'todos')
|
|
82
|
+
mkdirSync(todosDir, { recursive: true })
|
|
83
|
+
const todoPath = join(todosDir, `${date}.md`)
|
|
84
|
+
if (!existsSync(todoPath)) {
|
|
85
|
+
write(todoPath, buildTodayTodo(date), `todos/${date}.md`)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildCompanyMigi(businessType, goals, date) {
|
|
90
|
+
const personalizationNotes = buildPersonalizationNotes(businessType, goals)
|
|
91
|
+
const template = readFileSync(join(PACKAGE_DIR, 'templates', 'company-migi.md'), 'utf-8')
|
|
92
|
+
|
|
93
|
+
return template
|
|
94
|
+
.replace('{{BUSINESS_TYPE}}', businessType)
|
|
95
|
+
.replace('{{GOALS_AND_CHALLENGES}}', goals)
|
|
96
|
+
.replace('{{CREATED_DATE}}', date)
|
|
97
|
+
.replace('{{ADDITIONAL_DEPARTMENTS}}', '')
|
|
98
|
+
.replace('{{DEPARTMENT_TABLE_ROWS}}', '')
|
|
99
|
+
.replace('{{PERSONALIZATION_NOTES}}', personalizationNotes)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildPersonalizationNotes(businessType, goals) {
|
|
103
|
+
const notes = []
|
|
104
|
+
if (businessType) notes.push(`- 事業・活動: ${businessType}`)
|
|
105
|
+
if (goals) notes.push(`- 目標・課題: ${goals}`)
|
|
106
|
+
return notes.join('\n') || '(未設定)'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildTodayTodo(date) {
|
|
110
|
+
const weekdays = ['日', '月', '火', '水', '木', '金', '土']
|
|
111
|
+
const d = new Date(date)
|
|
112
|
+
const dow = weekdays[d.getDay()]
|
|
113
|
+
return `---\ndate: "${date}"\ntype: daily\n---\n\n# ${date} (${dow})\n\n## TODO\n\n\n`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function write(path, content, label) {
|
|
117
|
+
writeFileSync(path, content, 'utf-8')
|
|
118
|
+
console.log(chalk.dim(` ✓ ${label}`))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function today() {
|
|
122
|
+
return new Date().toISOString().split('T')[0]
|
|
123
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
// これらは確認なしで自動実行
|
|
4
|
+
const AUTO_APPROVED = new Set(['read_file', 'list_files', 'search_content'])
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* promptFn: (question: string) => Promise<string>
|
|
8
|
+
* bin/migi.js から readline の question 関数を受け取る
|
|
9
|
+
*/
|
|
10
|
+
export function createPermissionChecker(promptFn) {
|
|
11
|
+
return async function checkPermission(toolName, args) {
|
|
12
|
+
if (AUTO_APPROVED.has(toolName)) return true
|
|
13
|
+
|
|
14
|
+
console.log(chalk.yellow('\n ⚡ 実行確認'))
|
|
15
|
+
console.log(chalk.dim(` ツール : ${toolName}`))
|
|
16
|
+
if (args.path) console.log(chalk.dim(` パス : ${args.path}`))
|
|
17
|
+
if (args.command) console.log(chalk.dim(` コマンド: ${args.command}`))
|
|
18
|
+
|
|
19
|
+
const answer = await promptFn(chalk.yellow(' 実行しますか? [y/N] '))
|
|
20
|
+
return answer.trim().toLowerCase() === 'y'
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/setup.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { homedir } from 'os'
|
|
4
|
+
import readline from 'readline'
|
|
5
|
+
import chalk from 'chalk'
|
|
6
|
+
import OpenAI from 'openai'
|
|
7
|
+
|
|
8
|
+
export const MIGI_DIR = join(homedir(), '.migi')
|
|
9
|
+
export const CONFIG_PATH = join(MIGI_DIR, 'config.json')
|
|
10
|
+
|
|
11
|
+
export function loadGlobalConfig() {
|
|
12
|
+
if (!existsSync(CONFIG_PATH)) return null
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'))
|
|
15
|
+
} catch {
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function extractName(apiKey, model, input) {
|
|
21
|
+
if (!input) return 'Migi'
|
|
22
|
+
try {
|
|
23
|
+
const client = new OpenAI({ apiKey })
|
|
24
|
+
const res = await client.chat.completions.create({
|
|
25
|
+
model,
|
|
26
|
+
messages: [{
|
|
27
|
+
role: 'user',
|
|
28
|
+
content: `ユーザーが AI エージェントにつけたい名前を入力しました。\n入力: "${input}"\n\n入力から名前だけを抽出して、その名前のみを返してください。説明不要。`
|
|
29
|
+
}],
|
|
30
|
+
max_tokens: 20,
|
|
31
|
+
})
|
|
32
|
+
return res.choices[0].message.content.trim() || input
|
|
33
|
+
} catch {
|
|
34
|
+
return input
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function saveGlobalConfig(config) {
|
|
39
|
+
mkdirSync(MIGI_DIR, { recursive: true })
|
|
40
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function runSetup(promptFn = null) {
|
|
44
|
+
// readline を外から受け取るか、自前で作る
|
|
45
|
+
let rl = null
|
|
46
|
+
let ask = promptFn
|
|
47
|
+
if (!ask) {
|
|
48
|
+
rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
49
|
+
ask = (q) => new Promise((resolve) => rl.question(q, resolve))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---- 自己紹介 ----
|
|
53
|
+
console.log(chalk.bold.cyan('\n ╔══════════════════════════════════════╗'))
|
|
54
|
+
console.log(chalk.bold.cyan(' ║ Migi — by MAKE U FREE ║'))
|
|
55
|
+
console.log(chalk.bold.cyan(' ╚══════════════════════════════════════╝\n'))
|
|
56
|
+
console.log(chalk.white(' はじめまして!'))
|
|
57
|
+
console.log(chalk.white(' 私はあなたの右腕として動く AI エージェントです。\n'))
|
|
58
|
+
console.log(chalk.dim(' タスク管理・壁打ち・ファイル操作・コマンド実行...'))
|
|
59
|
+
console.log(chalk.dim(' 仕事も人生も、何でも一緒に動きます。\n'))
|
|
60
|
+
console.log(chalk.dim(' ─────────────────────────────────────\n'))
|
|
61
|
+
|
|
62
|
+
// ---- API キー ----
|
|
63
|
+
console.log(chalk.dim(' まず OpenAI API キーを設定しましょう。'))
|
|
64
|
+
console.log(chalk.dim(' 取得: https://platform.openai.com/api-keys\n'))
|
|
65
|
+
const apiKey = await ask(chalk.white(' API キー > '))
|
|
66
|
+
|
|
67
|
+
if (!apiKey.trim()) {
|
|
68
|
+
console.log(chalk.red('\n API キーが入力されていません。終了します。\n'))
|
|
69
|
+
if (rl) rl.close()
|
|
70
|
+
process.exit(1)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---- モデル選択 ----
|
|
74
|
+
console.log('')
|
|
75
|
+
console.log(chalk.dim(' 使用するモデルを選んでください。'))
|
|
76
|
+
console.log(chalk.dim(' 1) gpt-4o (高性能・推奨)'))
|
|
77
|
+
console.log(chalk.dim(' 2) gpt-4o-mini (高速・低コスト)'))
|
|
78
|
+
console.log(chalk.dim(' 3) その他 (直接入力)\n'))
|
|
79
|
+
const modelChoice = await ask(chalk.white(' 選択 [1] > '))
|
|
80
|
+
|
|
81
|
+
let model = 'gpt-4o'
|
|
82
|
+
if (modelChoice.trim() === '2') {
|
|
83
|
+
model = 'gpt-4o-mini'
|
|
84
|
+
} else if (modelChoice.trim() === '3') {
|
|
85
|
+
const custom = await ask(chalk.white(' モデル名 > '))
|
|
86
|
+
model = custom.trim() || 'gpt-4o'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---- 名前(AIで解釈) ----
|
|
90
|
+
console.log('')
|
|
91
|
+
console.log(chalk.white(' ひとつお願いがあります。'))
|
|
92
|
+
console.log(chalk.white(' ─── 名前をつけてもらえますか? ───\n'))
|
|
93
|
+
console.log(chalk.dim(' あなただけの右腕として、その名前で動きます。'))
|
|
94
|
+
console.log(chalk.dim(' 例: ミギ、アシ、レン、なんでも OK\n'))
|
|
95
|
+
|
|
96
|
+
const nameInput = await ask(chalk.cyan(' 名前 > '))
|
|
97
|
+
const name = await extractName(apiKey.trim(), model, nameInput.trim())
|
|
98
|
+
|
|
99
|
+
console.log(chalk.green(`\n ${name} です。よろしくお願いします!\n`))
|
|
100
|
+
|
|
101
|
+
// ---- 保存 ----
|
|
102
|
+
const config = { name, openai_api_key: apiKey.trim(), model }
|
|
103
|
+
saveGlobalConfig(config)
|
|
104
|
+
if (rl) rl.close()
|
|
105
|
+
|
|
106
|
+
console.log(chalk.dim(` 設定を保存しました: ${CONFIG_PATH}`))
|
|
107
|
+
console.log(chalk.dim(` 名前: ${name} / モデル: ${model}\n`))
|
|
108
|
+
console.log(chalk.cyan(' ─────────────────────────────────────\n'))
|
|
109
|
+
|
|
110
|
+
return config
|
|
111
|
+
}
|
package/src/skills.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs'
|
|
2
|
+
import { join, dirname } from 'path'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
|
|
5
|
+
const PACKAGE_DIR = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* スキルを解決して内容を返す
|
|
9
|
+
* 優先度: .migi/skills/{name}.md(ユーザー定義)> skills/{name}.md(ビルトイン)
|
|
10
|
+
*/
|
|
11
|
+
export function resolveSkill(name, cwd = process.cwd()) {
|
|
12
|
+
const candidates = [
|
|
13
|
+
join(cwd, '.migi', 'skills', `${name}.md`), // ユーザー定義
|
|
14
|
+
join(PACKAGE_DIR, 'skills', `${name}.md`), // ビルトイン
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
for (const path of candidates) {
|
|
18
|
+
if (existsSync(path)) {
|
|
19
|
+
return { content: readFileSync(path, 'utf-8'), path }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 入力が /コマンド 形式かチェックし、スキル名と残りの入力を返す
|
|
28
|
+
*/
|
|
29
|
+
export function parseSkillInput(input) {
|
|
30
|
+
if (!input.startsWith('/')) return null
|
|
31
|
+
const [cmd, ...rest] = input.slice(1).split(' ')
|
|
32
|
+
return { name: cmd, args: rest.join(' ') }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* スキルをメッセージに展開する
|
|
37
|
+
*/
|
|
38
|
+
export function expandSkill(skillContent, args) {
|
|
39
|
+
const argSection = args ? `\n\nユーザーの入力: ${args}` : ''
|
|
40
|
+
return `以下のスキル定義に従って動作してください:\n\n${skillContent}${argSection}`
|
|
41
|
+
}
|
package/src/tools.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from 'fs'
|
|
2
|
+
import { execSync } from 'child_process'
|
|
3
|
+
import { dirname } from 'path'
|
|
4
|
+
import { glob } from 'glob'
|
|
5
|
+
|
|
6
|
+
// ---- OpenAI ツールスキーマ定義 ----
|
|
7
|
+
|
|
8
|
+
export const toolSchemas = [
|
|
9
|
+
{
|
|
10
|
+
type: 'function',
|
|
11
|
+
function: {
|
|
12
|
+
name: 'read_file',
|
|
13
|
+
description: 'ファイルの内容を読み込む',
|
|
14
|
+
parameters: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
path: { type: 'string', description: 'ファイルパス(絶対パスまたは相対パス)' }
|
|
18
|
+
},
|
|
19
|
+
required: ['path']
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
type: 'function',
|
|
25
|
+
function: {
|
|
26
|
+
name: 'write_file',
|
|
27
|
+
description: 'ファイルに内容を書き込む(新規作成または上書き)',
|
|
28
|
+
parameters: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: {
|
|
31
|
+
path: { type: 'string', description: 'ファイルパス' },
|
|
32
|
+
content: { type: 'string', description: '書き込む内容' }
|
|
33
|
+
},
|
|
34
|
+
required: ['path', 'content']
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: 'function',
|
|
40
|
+
function: {
|
|
41
|
+
name: 'append_file',
|
|
42
|
+
description: 'ファイルの末尾に内容を追記する',
|
|
43
|
+
parameters: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
path: { type: 'string', description: 'ファイルパス' },
|
|
47
|
+
content: { type: 'string', description: '追記する内容' }
|
|
48
|
+
},
|
|
49
|
+
required: ['path', 'content']
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'function',
|
|
55
|
+
function: {
|
|
56
|
+
name: 'execute_command',
|
|
57
|
+
description: 'シェルコマンドを実行する',
|
|
58
|
+
parameters: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
command: { type: 'string', description: '実行するコマンド' }
|
|
62
|
+
},
|
|
63
|
+
required: ['command']
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: 'function',
|
|
69
|
+
function: {
|
|
70
|
+
name: 'list_files',
|
|
71
|
+
description: 'globパターンでファイルを一覧表示する',
|
|
72
|
+
parameters: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {
|
|
75
|
+
pattern: { type: 'string', description: 'globパターン(例: **/*.md)' },
|
|
76
|
+
cwd: { type: 'string', description: '検索ベースディレクトリ(省略時はカレントディレクトリ)' }
|
|
77
|
+
},
|
|
78
|
+
required: ['pattern']
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
type: 'function',
|
|
84
|
+
function: {
|
|
85
|
+
name: 'search_content',
|
|
86
|
+
description: 'ファイル内容をキーワードで検索する',
|
|
87
|
+
parameters: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
properties: {
|
|
90
|
+
pattern: { type: 'string', description: '検索パターン(正規表現可)' },
|
|
91
|
+
path: { type: 'string', description: '検索対象のファイルまたはディレクトリ' }
|
|
92
|
+
},
|
|
93
|
+
required: ['pattern', 'path']
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
// ---- ツール実行 ----
|
|
100
|
+
|
|
101
|
+
export async function executeTool(name, args) {
|
|
102
|
+
switch (name) {
|
|
103
|
+
case 'read_file': {
|
|
104
|
+
if (!existsSync(args.path)) return `エラー: ファイルが見つかりません: ${args.path}`
|
|
105
|
+
return readFileSync(args.path, 'utf-8')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
case 'write_file': {
|
|
109
|
+
const dir = dirname(args.path)
|
|
110
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
111
|
+
writeFileSync(args.path, args.content, 'utf-8')
|
|
112
|
+
return `完了: ${args.path} に書き込みました`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case 'append_file': {
|
|
116
|
+
const dir = dirname(args.path)
|
|
117
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
118
|
+
appendFileSync(args.path, args.content, 'utf-8')
|
|
119
|
+
return `完了: ${args.path} に追記しました`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
case 'execute_command': {
|
|
123
|
+
try {
|
|
124
|
+
const output = execSync(args.command, { encoding: 'utf-8', timeout: 30000 })
|
|
125
|
+
return output.trim() || '(出力なし)'
|
|
126
|
+
} catch (err) {
|
|
127
|
+
return `エラー: ${err.message}`
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
case 'list_files': {
|
|
132
|
+
const files = await glob(args.pattern, { cwd: args.cwd || process.cwd(), dot: true })
|
|
133
|
+
return files.length > 0 ? files.join('\n') : '(ファイルが見つかりませんでした)'
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
case 'search_content': {
|
|
137
|
+
try {
|
|
138
|
+
const output = execSync(
|
|
139
|
+
`grep -r ${JSON.stringify(args.pattern)} ${JSON.stringify(args.path)} --include="*.md" --include="*.txt" --include="*.js" --include="*.ts" -l 2>/dev/null`,
|
|
140
|
+
{ encoding: 'utf-8' }
|
|
141
|
+
)
|
|
142
|
+
return output.trim() || '(マッチなし)'
|
|
143
|
+
} catch {
|
|
144
|
+
return '(マッチなし)'
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
default:
|
|
149
|
+
return `不明なツール: ${name}`
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Migi - 仮想組織管理システム
|
|
2
|
+
|
|
3
|
+
## オーナープロフィール
|
|
4
|
+
|
|
5
|
+
- **事業・活動**: {{BUSINESS_TYPE}}
|
|
6
|
+
- **目標・課題**: {{GOALS_AND_CHALLENGES}}
|
|
7
|
+
- **作成日**: {{CREATED_DATE}}
|
|
8
|
+
|
|
9
|
+
## 組織構成
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
.company/
|
|
13
|
+
├── MIGI.md
|
|
14
|
+
└── secretary/
|
|
15
|
+
├── MIGI.md
|
|
16
|
+
├── inbox/
|
|
17
|
+
├── notes/
|
|
18
|
+
{{ADDITIONAL_DEPARTMENTS}}```
|
|
19
|
+
|
|
20
|
+
## 部署一覧
|
|
21
|
+
|
|
22
|
+
| 部署 | フォルダ | 役割 |
|
|
23
|
+
|------|---------|------|
|
|
24
|
+
| 秘書室 | secretary | 窓口・相談役。TODO管理、壁打ち、メモ。常設。 |
|
|
25
|
+
{{DEPARTMENT_TABLE_ROWS}}
|
|
26
|
+
|
|
27
|
+
## 運営ルール
|
|
28
|
+
|
|
29
|
+
### 秘書が窓口
|
|
30
|
+
- ユーザーとの対話は常に秘書が担当する
|
|
31
|
+
- 秘書は丁寧だが親しみやすい口調で話す
|
|
32
|
+
- 壁打ち、相談、雑談、何でも受け付ける
|
|
33
|
+
- 部署の作業が必要な場合、秘書が直接該当部署のフォルダに書き込む
|
|
34
|
+
|
|
35
|
+
### 自動記録
|
|
36
|
+
- 意思決定、学び、アイデアは言われなくても記録する
|
|
37
|
+
- 意思決定 → `secretary/notes/YYYY-MM-DD-decisions.md`
|
|
38
|
+
- 学び → `secretary/notes/YYYY-MM-DD-learnings.md`
|
|
39
|
+
- アイデア → `secretary/inbox/YYYY-MM-DD.md`
|
|
40
|
+
|
|
41
|
+
### TODOファイルの場所
|
|
42
|
+
- **日次TODOは `.company/` の外に置く**: `todos/YYYY-MM-DD.md`
|
|
43
|
+
|
|
44
|
+
### 同日1ファイル
|
|
45
|
+
- 同じ日付のファイルがすでに存在する場合は追記する。新規作成しない
|
|
46
|
+
|
|
47
|
+
### 日付チェック
|
|
48
|
+
- ファイル操作の前に必ず今日の日付を確認する
|
|
49
|
+
|
|
50
|
+
### ファイル命名規則
|
|
51
|
+
- **日次ファイル**: `YYYY-MM-DD.md`
|
|
52
|
+
- **トピックファイル**: `kebab-case-title.md`
|
|
53
|
+
|
|
54
|
+
### TODO形式
|
|
55
|
+
```markdown
|
|
56
|
+
- [ ] タスク内容 | 優先度: 高/通常/低 | 期限: YYYY-MM-DD
|
|
57
|
+
- [x] 完了タスク | 完了: YYYY-MM-DD
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### コンテンツルール
|
|
61
|
+
1. 迷ったら `secretary/inbox/` に入れる
|
|
62
|
+
2. 既存ファイルは上書きしない(追記のみ)
|
|
63
|
+
3. 追記時はタイムスタンプを付ける
|
|
64
|
+
|
|
65
|
+
## パーソナライズメモ
|
|
66
|
+
|
|
67
|
+
{{PERSONALIZATION_NOTES}}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# 秘書室
|
|
2
|
+
|
|
3
|
+
## 役割
|
|
4
|
+
オーナーの常駐窓口。何でも相談に乗り、タスク管理・壁打ち・メモを担当する。
|
|
5
|
+
|
|
6
|
+
## 口調・キャラクター
|
|
7
|
+
- 丁寧だが堅すぎない。「〜ですね!」「承知しました」「いいですね!」
|
|
8
|
+
- 主体的に提案する。「ついでにこれもやっておきましょうか?」
|
|
9
|
+
- 壁打ち時はカジュアルに寄り添う
|
|
10
|
+
- 過去のメモや決定事項を参照して文脈を持った対話をする
|
|
11
|
+
|
|
12
|
+
## ルール
|
|
13
|
+
- オーナーからの入力はまず秘書が受け取る
|
|
14
|
+
- 秘書で完結するもの(TODO、メモ、壁打ち、雑談)は直接対応
|
|
15
|
+
- 部署の作業が必要な場合は該当部署のフォルダに直接書き込む
|
|
16
|
+
- 該当部署が未作成の場合は secretary/notes/ に保存する
|
|
17
|
+
- TODO形式: `- [ ] タスク | 優先度: 高/通常/低 | 期限: YYYY-MM-DD`
|
|
18
|
+
- 日次TODOは `.company/` の外: `todos/YYYY-MM-DD.md`
|
|
19
|
+
- Inboxは `inbox/YYYY-MM-DD.md`。迷ったらまずここ
|
|
20
|
+
- 壁打ちの結論が出たら `notes/` への保存を提案する
|
|
21
|
+
- 意思決定は `notes/YYYY-MM-DD-decisions.md` に記録する
|
|
22
|
+
- 同じ日付のファイルがすでにある場合は追記する。新規作成しない
|
|
23
|
+
|
|
24
|
+
## 部署追加の提案
|
|
25
|
+
- 同じ領域のタスクが2回以上繰り返されたら、部署作成を提案する
|
|
26
|
+
- ユーザーが明示的に依頼した場合は即座に作成する
|
|
27
|
+
|
|
28
|
+
## フォルダ構成
|
|
29
|
+
- `inbox/` - 未整理のクイックキャプチャ
|
|
30
|
+
- `notes/` - 壁打ち・相談メモ・意思決定ログ
|