@make-u-free/migi 0.4.6 → 0.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.
Files changed (3) hide show
  1. package/bin/migi.js +18 -10
  2. package/package.json +1 -1
  3. package/src/agent.js +113 -19
package/bin/migi.js CHANGED
@@ -149,12 +149,11 @@ async function readChatInput() {
149
149
  }
150
150
 
151
151
  if (key.ctrl && key.name === 'c') {
152
- if (drawPending) { drawPending = false; draw() } // カーソル位置を確定させてから終了
152
+ if (drawPending) { drawPending = false; draw() }
153
153
  process.stdout.write(`\x1b[${drawnLines - 1 - curLine}B\n`)
154
154
  process.stdin.removeListener('keypress', onKey)
155
155
  if (process.stdin.isTTY) process.stdin.setRawMode(false)
156
- console.log(chalk.cyan('\n お疲れ様でした!またね。\n'))
157
- process.exit(0)
156
+ resolve(null) // null = 終了シグナル(メインループで後処理)
158
157
  }
159
158
 
160
159
  if (key.name === 'return') {
@@ -198,18 +197,29 @@ async function readChatInput() {
198
197
  })
199
198
  }
200
199
 
200
+ // ---- セッション終了(サマリー保存 → 挨拶 → exit) ----
201
+ async function gracefulExit() {
202
+ const saved = await agent.saveSummary(cwd)
203
+ if (saved) {
204
+ console.log(chalk.dim(`\n セッションを記録しました → ${saved}`))
205
+ }
206
+ console.log(chalk.cyan(`\n お疲れ様でした!またね。\n`))
207
+ process.exit(0)
208
+ }
209
+
201
210
  // ---- メインループ ----
202
211
  async function prompt() {
203
212
  // 入力ボックス上辺(ユーザー名をセパレーターに埋め込む)
204
213
  console.log('\n' + sepWithLabel(chalk.bold.cyan(userName || 'あなた')))
205
214
 
206
- const input = (await readChatInput()).trim()
215
+ const rawInput = await readChatInput()
216
+ if (rawInput === null) return gracefulExit() // Ctrl+C
217
+ const input = rawInput.trim()
207
218
  if (!input) return prompt()
208
219
 
209
220
  // --- ビルトインコマンド ---
210
221
  if (input === '/exit' || input === '/quit') {
211
- console.log(chalk.cyan(`\n お疲れ様でした!またね。\n`))
212
- process.exit(0)
222
+ return gracefulExit()
213
223
  }
214
224
 
215
225
  if (input === '/config') {
@@ -249,8 +259,7 @@ async function prompt() {
249
259
  console.log('\n' + sepWithLabel(chalk.bold.cyan(agentName) + chalk.dim(` [スキル: ${parsed.name}]`)))
250
260
  const expanded = expandSkill(skill.content, parsed.args)
251
261
  try {
252
- const reply = await agent.chat(expanded)
253
- console.log('\n' + reply + '\n')
262
+ await agent.chat(expanded)
254
263
  } catch (err) {
255
264
  console.error(chalk.red('\n エラー: ' + err.message + '\n'))
256
265
  }
@@ -265,8 +274,7 @@ async function prompt() {
265
274
  // --- 通常チャット ---
266
275
  console.log('\n' + sepWithLabel(chalk.bold.cyan(agentName)))
267
276
  try {
268
- const reply = await agent.chat(input)
269
- console.log('\n' + reply + '\n')
277
+ await agent.chat(input)
270
278
  } catch (err) {
271
279
  console.error(chalk.red('\n エラー: ' + err.message + '\n'))
272
280
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@make-u-free/migi",
3
- "version": "0.4.6",
3
+ "version": "0.5.0",
4
4
  "description": "Your AI right-hand agent. Works anywhere, with any LLM API.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/agent.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import OpenAI from 'openai'
2
2
  import chalk from 'chalk'
3
3
  import { homedir } from 'os'
4
+ import { existsSync, appendFileSync, mkdirSync } from 'fs'
5
+ import { join, dirname } from 'path'
4
6
  import { toolSchemas, teamsToolSchema, executeTool } from './tools.js'
5
7
  import { createPermissionChecker } from './permissions.js'
6
8
  import { httpsAgent } from './tls.js'
@@ -45,14 +47,18 @@ ${userNameLine}
45
47
  - 「どうしますか?」と聞く前に、自分でできることをやりきる
46
48
  - 完了したらまとめて報告する。途中経過は簡潔に
47
49
 
48
- ## メモリ
49
- - ユーザーが「覚えておいて」「記録して」「remember」と言ったら、必ず memory.md に書き出す
50
- - グローバルメモリ: ${homedir()}/.migi/memory.md(どのワークスペースでも使う情報)
51
- - ワークスペースメモリ: ${cwd}/.migi/memory.md(このプロジェクト固有の情報)
52
- - 迷ったらグローバルメモリに書く
53
- - 形式: "## YYYY-MM-DD" の見出しの下に箇条書きで記録
54
- - 既存ファイルがあれば追記、なければ新規作成
55
- - 重要な意思決定・学び・好みは言われなくても「記録しておきましょうか?」と提案する
50
+ ## メモリと文脈の継続
51
+ - グローバルメモリ: ${homedir()}/.migi/memory.md(ユーザーの好み・習慣・横断的な情報)
52
+ - ワークスペースメモリ: ${cwd}/.migi/memory.md(このプロジェクト固有の情報・決定事項)
53
+ - 形式: "## YYYY-MM-DD" の見出しの下に箇条書きで記録。既存ファイルがあれば追記
54
+ - ユーザーが「覚えておいて」「remember」と言ったら必ず書き出す
55
+ - 言われなくても、以下は自発的に記録する:
56
+ - 重要な意思決定・方針転換
57
+ - ユーザーの好み・こだわり・やり方のクセ
58
+ - 繰り返し登場するテーマやプロジェクト
59
+ - 「次回やること」として明確になったタスク
60
+ - セッション開始時にメモリの内容を参照し、前回の続きから自然に入る
61
+ - 過去の記録と矛盾することをユーザーが言ったら「前回と変わりましたか?」と確認する
56
62
 
57
63
  ## 環境
58
64
  - 今日の日付: ${new Date().toISOString().split('T')[0]}
@@ -67,6 +73,53 @@ ${userNameLine}
67
73
  (context ? `\n## ロードされたコンテキスト\n${context}` : '')
68
74
  }
69
75
 
76
+ // セッションの会話をサマリーして memory.md に保存する
77
+ async saveSummary(cwd) {
78
+ // ユーザー発言が2回未満なら保存しない(短すぎるセッション)
79
+ const userTurns = this.history.filter(m => m.role === 'user').length
80
+ if (userTurns < 2) return null
81
+
82
+ const spinner = new Spinner()
83
+ spinner.start('セッションを記録中…')
84
+
85
+ try {
86
+ const response = await this.client.chat.completions.create({
87
+ model: this.model,
88
+ messages: [
89
+ { role: 'system', content: this.systemPrompt },
90
+ ...this.history,
91
+ {
92
+ role: 'user',
93
+ content: `このセッションを次回の文脈引き継ぎ用に要約してください。
94
+ 以下の形式で箇条書き3〜6行。日本語で簡潔に(1行50字以内)。
95
+
96
+ - 話し合ったこと・決定したこと
97
+ - 完了したこと・作ったもの
98
+ - ユーザーについて学んだこと(好み・やり方など)
99
+ - 次回やること(あれば)
100
+
101
+ 形式:「- 〜」の箇条書きのみ。見出しや前置きは不要。`
102
+ }
103
+ ]
104
+ })
105
+
106
+ const summary = response.choices[0].message.content.trim()
107
+ const today = new Date().toISOString().split('T')[0]
108
+ const entry = `\n## ${today}\n${summary}\n`
109
+
110
+ const memPath = join(cwd, '.migi', 'memory.md')
111
+ const dir = dirname(memPath)
112
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
113
+ appendFileSync(memPath, entry, 'utf-8')
114
+
115
+ spinner.stop()
116
+ return memPath
117
+ } catch (err) {
118
+ spinner.stop()
119
+ return null
120
+ }
121
+ }
122
+
70
123
  // tool_calls に対応する tool 結果がない壊れた履歴を修復する
71
124
  _sanitizeHistory() {
72
125
  const cleaned = []
@@ -97,28 +150,69 @@ ${userNameLine}
97
150
 
98
151
  while (true) {
99
152
  spinner.start('考え中…')
100
- const response = await this.client.chat.completions.create({
153
+
154
+ const stream = await this.client.chat.completions.create({
101
155
  model: this.model,
102
156
  messages,
103
157
  tools: this.tools,
104
- tool_choice: 'auto'
158
+ tool_choice: 'auto',
159
+ stream: true
105
160
  })
106
- spinner.stop()
107
161
 
108
- const choice = response.choices[0]
109
- messages.push(choice.message)
110
- this.history.push(choice.message)
162
+ let content = ''
163
+ const tcMap = {} // tool_calls をインデックスで蓄積
164
+ let finishReason = null
165
+ let streaming = false // 最初のコンテンツが届いたか
166
+
167
+ for await (const chunk of stream) {
168
+ const choice = chunk.choices[0]
169
+ if (!choice) continue
170
+ const delta = choice.delta
171
+ if (choice.finish_reason) finishReason = choice.finish_reason
172
+
173
+ // テキストチャンク
174
+ if (delta?.content) {
175
+ if (!streaming) {
176
+ spinner.stop()
177
+ process.stdout.write('\n')
178
+ streaming = true
179
+ }
180
+ content += delta.content
181
+ process.stdout.write(delta.content)
182
+ }
183
+
184
+ // tool_calls チャンク(引数はストリームで分割されて届く)
185
+ if (delta?.tool_calls) {
186
+ for (const tc of delta.tool_calls) {
187
+ if (!tcMap[tc.index]) tcMap[tc.index] = { id: '', type: 'function', function: { name: '', arguments: '' } }
188
+ if (tc.id) tcMap[tc.index].id += tc.id
189
+ if (tc.function?.name) tcMap[tc.index].function.name += tc.function.name
190
+ if (tc.function?.arguments) tcMap[tc.index].function.arguments += tc.function.arguments
191
+ }
192
+ }
193
+ }
194
+
195
+ spinner.stop()
111
196
 
112
197
  // 通常の返答
113
- if (choice.finish_reason === 'stop') {
114
- return choice.message.content
198
+ if (finishReason === 'stop') {
199
+ process.stdout.write('\n\n')
200
+ const assistantMsg = { role: 'assistant', content }
201
+ messages.push(assistantMsg)
202
+ this.history.push(assistantMsg)
203
+ return content
115
204
  }
116
205
 
117
206
  // ツール呼び出し
118
- if (choice.finish_reason === 'tool_calls') {
119
- const toolResults = []
207
+ if (finishReason === 'tool_calls') {
208
+ if (streaming) process.stdout.write('\n')
209
+ const toolCalls = Object.values(tcMap)
210
+ const assistantMsg = { role: 'assistant', content: content || null, tool_calls: toolCalls }
211
+ messages.push(assistantMsg)
212
+ this.history.push(assistantMsg)
120
213
 
121
- for (const toolCall of choice.message.tool_calls) {
214
+ const toolResults = []
215
+ for (const toolCall of toolCalls) {
122
216
  const args = JSON.parse(toolCall.function.arguments)
123
217
  const name = toolCall.function.name
124
218