@askdkc/kiokuko 0.1.2 → 0.1.4

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 (58) hide show
  1. package/README.ja.md +17 -11
  2. package/README.ko.md +17 -11
  3. package/README.md +29 -12
  4. package/README.zh-CN.md +17 -11
  5. package/dist/agent-file/render.d.ts +1 -1
  6. package/dist/agent-file/render.d.ts.map +1 -1
  7. package/dist/agent-file/render.js +7 -5
  8. package/dist/agent-file/render.js.map +1 -1
  9. package/dist/bin/kiokuko.js +11 -4
  10. package/dist/bin/kiokuko.js.map +1 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +10 -3
  13. package/dist/cli.js.map +1 -1
  14. package/dist/commands/init.d.ts +1 -0
  15. package/dist/commands/init.d.ts.map +1 -1
  16. package/dist/commands/init.js +19 -2
  17. package/dist/commands/init.js.map +1 -1
  18. package/dist/commands/setup.d.ts +3 -1
  19. package/dist/commands/setup.d.ts.map +1 -1
  20. package/dist/commands/setup.js +8 -2
  21. package/dist/commands/setup.js.map +1 -1
  22. package/dist/config/paths.d.ts +1 -0
  23. package/dist/config/paths.d.ts.map +1 -1
  24. package/dist/config/paths.js +5 -0
  25. package/dist/config/paths.js.map +1 -1
  26. package/dist/db/connection.d.ts.map +1 -1
  27. package/dist/db/connection.js +4 -2
  28. package/dist/db/connection.js.map +1 -1
  29. package/dist/db/migrate.d.ts +7 -0
  30. package/dist/db/migrate.d.ts.map +1 -1
  31. package/dist/db/migrate.js +76 -4
  32. package/dist/db/migrate.js.map +1 -1
  33. package/dist/db/upgrade-backup.d.ts +3 -0
  34. package/dist/db/upgrade-backup.d.ts.map +1 -0
  35. package/dist/db/upgrade-backup.js +43 -0
  36. package/dist/db/upgrade-backup.js.map +1 -0
  37. package/dist/mcp/server.js +3 -3
  38. package/dist/mcp/server.js.map +1 -1
  39. package/dist/runtime-version.d.ts +5 -0
  40. package/dist/runtime-version.d.ts.map +1 -0
  41. package/dist/runtime-version.js +16 -0
  42. package/dist/runtime-version.js.map +1 -0
  43. package/dist/setup/opencode-loop-guard.d.ts +4 -0
  44. package/dist/setup/opencode-loop-guard.d.ts.map +1 -0
  45. package/dist/setup/opencode-loop-guard.js +168 -0
  46. package/dist/setup/opencode-loop-guard.js.map +1 -0
  47. package/dist/setup/render.d.ts.map +1 -1
  48. package/dist/setup/render.js +7 -5
  49. package/dist/setup/render.js.map +1 -1
  50. package/dist/web/i18n.d.ts +83 -0
  51. package/dist/web/i18n.d.ts.map +1 -0
  52. package/dist/web/i18n.js +329 -0
  53. package/dist/web/i18n.js.map +1 -0
  54. package/dist/web/ui.d.ts.map +1 -1
  55. package/dist/web/ui.js +196 -65
  56. package/dist/web/ui.js.map +1 -1
  57. package/package.json +3 -3
  58. package/templates/AGENTS.md +7 -5
@@ -1 +1 @@
1
- {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../../src/web/ui.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,QAAQ,QA6Qb,CAAC"}
1
+ {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../../src/web/ui.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,QAAQ,QAoYb,CAAC"}
package/dist/web/ui.js CHANGED
@@ -1,5 +1,17 @@
1
+ import { DEFAULT_WEB_LOCALE, WEB_LOCALE_LABELS, WEB_LOCALES, WEB_MESSAGES } from './i18n.js';
2
+ const WEB_I18N_CONFIG = JSON.stringify({
3
+ defaultLocale: DEFAULT_WEB_LOCALE,
4
+ localeLabels: WEB_LOCALE_LABELS,
5
+ locales: WEB_LOCALES,
6
+ messages: WEB_MESSAGES,
7
+ })
8
+ .replaceAll('&', '\\u0026')
9
+ .replaceAll('<', '\\u003c')
10
+ .replaceAll('>', '\\u003e')
11
+ .replaceAll('\u2028', '\\u2028')
12
+ .replaceAll('\u2029', '\\u2029');
1
13
  export const WEB_HTML = String.raw `<!doctype html>
2
- <html lang="ja">
14
+ <html lang="en">
3
15
  <head>
4
16
  <meta charset="utf-8">
5
17
  <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -7,15 +19,29 @@ export const WEB_HTML = String.raw `<!doctype html>
7
19
  <style>
8
20
  :root { color-scheme: light; --ink:#1e293b; --muted:#64748b; --line:#e2e8f0; --panel:#ffffff; --surface:#f8fafc; --accent:#2563eb; --accent-soft:#eff6ff; --warn:#b45309; --danger:#b91c1c; }
9
21
  * { box-sizing:border-box; }
22
+ [hidden] { display:none !important; }
10
23
  body { margin:0; background:linear-gradient(135deg,#f8fafc,#eef2ff); color:var(--ink); font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
11
24
  button,input,select,textarea { font:inherit; }
12
25
  button { cursor:pointer; }
13
26
  .shell { max-width:1440px; margin:0 auto; padding:28px; }
14
- .topbar { display:flex; gap:18px; justify-content:space-between; align-items:flex-end; margin-bottom:22px; }
27
+ .topbar { display:grid; grid-template-columns:minmax(0,1.1fr) minmax(420px,.9fr); gap:24px; align-items:start; margin-bottom:22px; }
28
+ .brand,.topbar-side { min-width:0; }
29
+ .topbar-side { display:flex; flex-direction:column; gap:10px; align-self:stretch; }
15
30
  .eyebrow { color:var(--accent); font-size:12px; font-weight:800; letter-spacing:.16em; text-transform:uppercase; }
16
31
  h1 { margin:4px 0 0; font-size:clamp(28px,4vw,44px); letter-spacing:-.04em; }
17
32
  .subtitle { margin:8px 0 0; color:var(--muted); }
18
- .toolbar { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
33
+ .language-picker { position:relative; align-self:flex-end; z-index:10; }
34
+ .language-toggle { display:grid; place-items:center; width:44px; height:44px; border:1px solid var(--line); border-radius:12px; background:var(--panel); color:var(--ink); box-shadow:0 6px 18px rgba(15,23,42,.06); }
35
+ .language-toggle:hover,.language-toggle[aria-expanded="true"] { border-color:#93c5fd; background:var(--accent-soft); color:var(--accent); }
36
+ .language-toggle:focus-visible,.language-option:focus-visible { outline:3px solid rgba(37,99,235,.25); outline-offset:2px; }
37
+ .language-toggle svg { width:22px; height:22px; }
38
+ .language-menu { position:absolute; top:calc(100% + 8px); right:0; display:grid; gap:2px; width:max-content; min-width:168px; padding:6px; border:1px solid var(--line); border-radius:14px; background:var(--panel); box-shadow:0 18px 48px rgba(15,23,42,.16); }
39
+ .language-option { display:flex; align-items:center; justify-content:space-between; gap:18px; width:100%; border:0; border-radius:9px; padding:9px 11px; background:transparent; color:var(--ink); text-align:left; }
40
+ .language-option:hover,.language-option:focus-visible,.language-option[aria-checked="true"] { background:var(--accent-soft); color:var(--accent); }
41
+ .language-option[aria-checked="true"]::after { content:"✓"; font-weight:800; }
42
+ .toolbar { display:flex; gap:10px; align-items:center; justify-content:flex-end; flex-wrap:wrap; margin-top:auto; }
43
+ .topbar-side .control { flex:1 1 210px; width:auto; min-width:0; }
44
+ .topbar-side .search { flex:1.4 1 240px; width:auto; min-width:0; }
19
45
  .control, .search { border:1px solid var(--line); border-radius:12px; background:var(--panel); color:var(--ink); padding:10px 12px; }
20
46
  .search { min-width:260px; }
21
47
  .button { border:1px solid var(--line); border-radius:12px; background:var(--panel); color:var(--ink); padding:10px 14px; font-weight:700; }
@@ -56,7 +82,7 @@ export const WEB_HTML = String.raw `<!doctype html>
56
82
  .notice { border-radius:12px; padding:11px 12px; background:#fffbeb; color:var(--warn); font-size:13px; }
57
83
  .notice.error { background:#fef2f2; color:var(--danger); }
58
84
  .status { color:var(--muted); font-size:12px; min-height:18px; }
59
- .operator-panel { grid-column:1 / -1; }
85
+ .operator-panel { grid-column:1 / -1; margin-top:16px; }
60
86
  .operator-grid { display:grid; grid-template-columns:minmax(240px,.7fr) minmax(420px,1.3fr); gap:16px; }
61
87
  .run-list { display:flex; flex-direction:column; gap:8px; max-height:360px; overflow:auto; }
62
88
  .run-card { border:1px solid var(--line); border-radius:12px; padding:11px; background:var(--panel); text-align:left; }
@@ -66,63 +92,146 @@ export const WEB_HTML = String.raw `<!doctype html>
66
92
  .detail-text { white-space:pre-wrap; overflow-wrap:anywhere; color:var(--muted); font-size:13px; }
67
93
  @media (max-width:680px) { .operator-grid { grid-template-columns:1fr; } }
68
94
  @media (max-width:1050px) { .layout { grid-template-columns:180px minmax(280px,1fr); } .editor-panel { grid-column:1 / -1; } }
69
- @media (max-width:680px) { .shell { padding:16px; } .topbar { display:block; } .toolbar { margin-top:16px; } .search { min-width:0; flex:1; } .layout { grid-template-columns:1fr; } .genres-panel { order:0; } .list-panel { order:1; } .editor-panel { order:2; } .entry-list { max-height:none; } }
95
+ @media (max-width:680px) { .shell { padding:16px; } .topbar { display:block; position:relative; } .brand { padding-right:56px; } .topbar-side { margin-top:16px; align-self:auto; } .language-picker { position:absolute; top:0; right:0; } .toolbar { justify-content:stretch; margin-top:0; } .topbar-side .control,.topbar-side .search { flex:1 1 100%; width:100%; } .search { min-width:0; } .layout { grid-template-columns:1fr; } .genres-panel { order:0; } .list-panel { order:1; } .editor-panel { order:2; } .entry-list { max-height:none; } }
70
96
  </style>
71
97
  </head>
72
98
  <body>
73
99
  <main class="shell">
74
100
  <header class="topbar">
75
- <div>
76
- <div class="eyebrow">Local memory console</div>
101
+ <div class="brand">
102
+ <div class="eyebrow" data-i18n="eyebrow">Local memory console</div>
77
103
  <h1>Kiokuko Web</h1>
78
- <p class="subtitle">Bot用途・記憶タイプ・横断タグからSQLiteの記憶を確認し、安全に候補エントリを編集します。</p>
104
+ <p class="subtitle" data-i18n="subtitle">Browse SQLite memory by role and purpose, memory type, and cross-cutting tags, and safely edit candidate entries.</p>
79
105
  </div>
80
- <div class="toolbar">
81
- <select id="workspace" class="control" aria-label="ワークスペース"></select>
82
- <input id="search" class="search" type="search" placeholder="記憶を検索…" aria-label="記憶を検索">
83
- <button id="refresh" class="button" type="button">更新</button>
106
+ <div class="topbar-side">
107
+ <div id="language-picker" class="language-picker">
108
+ <button id="language-toggle" class="language-toggle" type="button" aria-haspopup="menu" aria-expanded="false" aria-controls="language-menu" aria-label="Language" data-i18n-aria-label="languageLabel">
109
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"></circle><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"></path></svg>
110
+ </button>
111
+ <div id="language-menu" class="language-menu" role="menu" aria-label="Language" data-i18n-aria-label="languageLabel" hidden></div>
112
+ </div>
113
+ <div class="toolbar">
114
+ <select id="workspace" class="control" aria-label="Workspace" data-i18n-aria-label="workspaceLabel"></select>
115
+ <input id="search" class="search" type="search" placeholder="Search memory…" aria-label="Search memory…" data-i18n-placeholder="searchPlaceholder" data-i18n-aria-label="searchPlaceholder">
116
+ <button id="refresh" class="button" type="button" data-i18n="refresh">Refresh</button>
117
+ </div>
84
118
  </div>
85
119
  </header>
86
120
  <div id="status" class="status" role="status"></div>
87
121
  <section class="layout">
88
122
  <aside class="panel genres-panel">
89
- <div class="panel-head"><h2>Bot用途 / タグ</h2></div>
90
- <nav id="genres" class="genres" aria-label="Bot用途・記憶タイプ・タグのフィルター"></nav>
123
+ <div class="panel-head"><h2 data-i18n="filtersPanelTitle">Role and purpose / tags</h2></div>
124
+ <nav id="genres" class="genres" aria-label="Filter by role and purpose, memory type, or tag" data-i18n-aria-label="filtersNavLabel"></nav>
91
125
  </aside>
92
126
  <section class="panel list-panel">
93
- <div class="panel-head"><h2 id="list-title">記憶</h2><span id="result-count" class="badge">0件</span></div>
127
+ <div class="panel-head"><h2 id="list-title">Memory</h2><span id="result-count" class="badge">0 items</span></div>
94
128
  <div class="panel-body"><div id="entry-list" class="entry-list"></div></div>
95
129
  </section>
96
130
  <section class="panel editor-panel">
97
- <div class="panel-head"><h2>内容</h2><span id="editor-state" class="badge">未選択</span></div>
131
+ <div class="panel-head"><h2 data-i18n="editorTitle">Details</h2><span id="editor-state" class="badge" data-i18n="unselected">Not selected</span></div>
98
132
  <div class="panel-body"><div id="editor"></div></div>
99
133
  </section>
100
134
  </section>
101
135
  <section class="panel operator-panel">
102
- <div class="panel-head"><h2>Agent run operator view</h2><span class="badge">stored data is untrusted / non-actionable</span></div>
136
+ <div class="panel-head"><h2 data-i18n="operatorTitle">Agent run operator view</h2><span class="badge" data-i18n="trustBadge">stored data is untrusted / non-actionable</span></div>
103
137
  <div class="panel-body operator-grid">
104
138
  <div><div id="run-list" class="run-list"></div><div id="run-page" class="status"></div></div>
105
- <div id="run-detail"><div class="editor-empty">runを選択すると intakeprofiletimelinedeliveryfeedbackcoverage を表示します。</div></div>
139
+ <div id="run-detail"><div class="editor-empty" data-i18n="runSelectFull">Select a run to view its intake, profile, timeline, delivery, feedback, and coverage.</div></div>
106
140
  </div>
107
141
  </section>
108
142
  </main>
109
143
  <script>
144
+ const i18n = ${WEB_I18N_CONFIG};
145
+ const localeStorageKey = 'kiokuko.web.locale';
146
+ const normalizeLocale = (value) => {
147
+ if (typeof value !== 'string') return null;
148
+ const normalized = value.trim().replaceAll('_', '-').toLowerCase();
149
+ if (!normalized) return null;
150
+ const parts = normalized.split('-');
151
+ const language = parts[0];
152
+ if (language === 'en' || language === 'ja' || language === 'ko') return language;
153
+ if (language === 'zh' && (parts.length === 1 || parts.includes('hans') || parts.includes('cn') || parts.includes('sg'))) return 'zh-CN';
154
+ return null;
155
+ };
156
+ const readStoredLocale = () => { try { return localStorage.getItem(localeStorageKey); } catch { return null; } };
157
+ const browserLocales = Array.isArray(navigator.languages) ? navigator.languages : [navigator.language];
158
+ const localeCandidates = [new URLSearchParams(location.search).get('lang'), readStoredLocale(), ...browserLocales];
159
+ const initialLocale = localeCandidates.map(normalizeLocale).find(Boolean) || i18n.defaultLocale;
110
160
  const kinds = [
111
- ['all', 'すべて'], ['fact', '事実'], ['decision', '決定'], ['lesson', '教訓'], ['preference', '好み'], ['reference', '参照']
161
+ ['all', 'kind.all'], ['fact', 'kind.fact'], ['decision', 'kind.decision'], ['lesson', 'kind.lesson'], ['preference', 'kind.preference'], ['reference', 'kind.reference']
112
162
  ];
113
163
  const botModes = [
114
- ['bot:common', '共通'], ['bot:researcher', 'Researcher'], ['bot:builder', 'Builder'], ['bot:reviewer', 'Reviewer'], ['bot:devops', 'DevOps'], ['bot:writer', 'Writer'], ['bot:analyst', 'Analyst']
164
+ ['bot:common', 'bot.common'], ['bot:researcher', 'bot.researcher'], ['bot:builder', 'bot.builder'], ['bot:reviewer', 'bot.reviewer'], ['bot:devops', 'bot.devops'], ['bot:writer', 'bot.writer'], ['bot:analyst', 'bot.analyst']
115
165
  ];
116
- const state = { workspace: '', kind: 'all', tag: '', query: '', entries: [], tags: [], selected: null, runs: [], selectedRun: null };
166
+ const state = { locale: initialLocale, workspace: '', kind: 'all', tag: '', query: '', entries: [], tags: [], selected: null, runs: [], selectedRun: null, localizedStatus: null };
117
167
  const $ = (id) => document.getElementById(id);
168
+ const t = (key, parameters = {}) => {
169
+ const template = i18n.messages[state.locale]?.[key] ?? i18n.messages[i18n.defaultLocale]?.[key] ?? key;
170
+ return template.replace(/\{([^}]+)\}/g, (_match, name) => Object.prototype.hasOwnProperty.call(parameters, name) ? String(parameters[name]) : '');
171
+ };
172
+ const tp = (key, count) => {
173
+ const category = new Intl.PluralRules(state.locale).select(count) === 'one' ? 'one' : 'other';
174
+ return t(key + '.' + category, { count });
175
+ };
176
+ const setI18nText = (element, key) => { element.dataset.i18n = key; element.textContent = t(key); return element; };
177
+ const labelForKind = (kind) => { const item = kinds.find(([value]) => value === kind); return item ? t(item[1]) : kind; };
178
+ const labelForStatus = (status) => i18n.messages[state.locale]?.['status.' + status] ?? status;
179
+ const updateEditorState = () => {
180
+ const element = $('editor-state');
181
+ if (state.selected) { delete element.dataset.i18n; element.dataset.i18nStatus = state.selected.status; element.textContent = labelForStatus(state.selected.status); }
182
+ else { delete element.dataset.i18nStatus; setI18nText(element, 'unselected'); }
183
+ };
184
+ const showStatus = (message, error = false) => { $('status').textContent = message; $('status').className = error ? 'status notice error' : 'status'; };
185
+ const setStatus = (message, error = false) => { state.localizedStatus = null; showStatus(message, error); };
186
+ const setLocalizedStatus = (key, parameters = {}, error = false) => { state.localizedStatus = { key, parameters, error }; showStatus(t(key, parameters), error); };
187
+ const setLocalizedCountStatus = (key, count, error = false) => { state.localizedStatus = { key, count, error, plural: true }; showStatus(tp(key, count), error); };
188
+ const renderLanguageMenu = () => {
189
+ const menu = $('language-menu');
190
+ menu.replaceChildren(...i18n.locales.map((locale) => {
191
+ const option = document.createElement('button');
192
+ option.type = 'button';
193
+ option.className = 'language-option';
194
+ option.lang = locale;
195
+ option.dataset.locale = locale;
196
+ option.setAttribute('role', 'menuitemradio');
197
+ option.setAttribute('aria-checked', String(locale === state.locale));
198
+ option.textContent = i18n.localeLabels[locale];
199
+ option.addEventListener('click', () => selectLocale(locale));
200
+ return option;
201
+ }));
202
+ };
203
+ const setLanguageMenuOpen = (open, restoreFocus = false) => {
204
+ const menu = $('language-menu');
205
+ const toggle = $('language-toggle');
206
+ menu.hidden = !open;
207
+ toggle.setAttribute('aria-expanded', String(open));
208
+ if (open) requestAnimationFrame(() => menu.querySelector('[aria-checked="true"]')?.focus());
209
+ else if (restoreFocus) toggle.focus();
210
+ };
211
+ const selectLocale = (value) => {
212
+ state.locale = normalizeLocale(value) || i18n.defaultLocale;
213
+ try { localStorage.setItem(localeStorageKey, state.locale); } catch {}
214
+ setLanguageMenuOpen(false, true);
215
+ applyTranslations(); renderFilters(); renderEntries(); renderRuns(); renderRunDetail(); updateEditorState();
216
+ };
217
+ const applyTranslations = () => {
218
+ document.documentElement.lang = state.locale;
219
+ document.querySelectorAll('[data-i18n]').forEach((element) => { element.textContent = t(element.dataset.i18n); });
220
+ document.querySelectorAll('[data-i18n-aria-label]').forEach((element) => { element.setAttribute('aria-label', t(element.dataset.i18nAriaLabel)); });
221
+ document.querySelectorAll('[data-i18n-placeholder]').forEach((element) => { element.setAttribute('placeholder', t(element.dataset.i18nPlaceholder)); });
222
+ document.querySelectorAll('[data-i18n-status]').forEach((element) => {
223
+ const value = labelForStatus(element.dataset.i18nStatus);
224
+ if ('value' in element) element.value = value; else element.textContent = value;
225
+ });
226
+ renderLanguageMenu();
227
+ if (state.localizedStatus) showStatus(state.localizedStatus.plural ? tp(state.localizedStatus.key, state.localizedStatus.count) : t(state.localizedStatus.key, state.localizedStatus.parameters), state.localizedStatus.error);
228
+ };
118
229
  const api = async (path, options) => {
119
230
  const response = await fetch(path, options);
120
231
  const value = await response.json();
121
- if (!response.ok) throw new Error(value.error?.message || 'リクエストに失敗しました');
232
+ if (!response.ok) throw new Error(value.error?.message || t('requestFailed'));
122
233
  return value;
123
234
  };
124
- const setStatus = (message, error = false) => { $('status').textContent = message; $('status').className = error ? 'status notice error' : 'status'; };
125
- const labelForKind = (kind) => (kinds.find((item) => item[0] === kind) || [kind, kind])[1];
126
235
  const escapeTags = (value) => value.split(',').map((item) => item.trim()).filter(Boolean);
127
236
 
128
237
  function filterButton(key, label, count, active, onClick) {
@@ -145,32 +254,32 @@ export const WEB_HTML = String.raw `<!doctype html>
145
254
  function renderFilters() {
146
255
  const kindCounts = Object.fromEntries(kinds.map(([kind]) => [kind, kind === 'all' ? state.entries.length : state.entries.filter((entry) => entry.kind === kind).length]));
147
256
  const tagCounts = Object.fromEntries(state.tags.map((item) => [item.tag, item.count]));
148
- const botButtons = botModes.map(([tag, label]) => filterButton(tag, label, tagCounts[tag], state.tag === tag, () => { state.tag = tag; renderFilters(); loadEntries(); }));
149
- const kindButtons = kinds.map(([kind, label]) => filterButton(kind, label, kindCounts[kind], state.kind === kind, () => { state.kind = kind; renderFilters(); loadEntries(); }));
257
+ const botButtons = botModes.map(([tag, label]) => filterButton(tag, t(label), tagCounts[tag], state.tag === tag, () => { state.tag = tag; renderFilters(); loadEntries(); }));
258
+ const kindButtons = kinds.map(([kind, label]) => filterButton(kind, t(label), kindCounts[kind], state.kind === kind, () => { state.kind = kind; renderFilters(); loadEntries(); }));
150
259
  const tagButtons = state.tags
151
260
  .filter((item) => !botModes.some(([tag]) => tag === item.tag))
152
261
  .map((item) => filterButton(item.tag, '#' + item.tag, item.count, state.tag === item.tag, () => { state.tag = item.tag; renderFilters(); loadEntries(); }));
153
- const root = $('genres'); root.replaceChildren(filterGroup('Bot用途(タグ)', botButtons), filterGroup('記憶タイプ', kindButtons));
154
- if (tagButtons.length > 0) root.append(filterGroup('横断タグ', tagButtons));
262
+ const root = $('genres'); root.replaceChildren(filterGroup(t('botFilterTitle'), botButtons), filterGroup(t('memoryTypeFilterTitle'), kindButtons));
263
+ if (tagButtons.length > 0) root.append(filterGroup(t('crossTagFilterTitle'), tagButtons));
155
264
  }
156
265
 
157
266
  function renderEntries() {
158
- $('result-count').textContent = String(state.entries.length) + '件';
159
- $('list-title').textContent = state.tag ? '#' + state.tag : (state.kind === 'all' ? '記憶' : labelForKind(state.kind));
267
+ $('result-count').textContent = tp('entryCount', state.entries.length);
268
+ $('list-title').textContent = state.tag ? '#' + state.tag : (state.kind === 'all' ? t('entriesTitle') : labelForKind(state.kind));
160
269
  const list = $('entry-list');
161
270
  if (!state.entries.length) {
162
- const empty = document.createElement('div'); empty.className = 'editor-empty'; empty.textContent = '該当する記憶はありません。'; list.replaceChildren(empty); return;
271
+ const empty = document.createElement('div'); empty.className = 'editor-empty'; setI18nText(empty, 'noEntries'); list.replaceChildren(empty); return;
163
272
  }
164
273
  list.replaceChildren(...state.entries.map((entry) => {
165
274
  const card = document.createElement('article'); card.className = 'entry-card ' + (state.selected?.id === entry.id ? 'selected' : '');
166
275
  const meta = document.createElement('div'); meta.className = 'entry-meta';
167
276
  const kind = document.createElement('span'); kind.className = 'badge'; kind.textContent = labelForKind(entry.kind);
168
- const status = document.createElement('span'); status.className = 'badge ' + entry.status; status.textContent = entry.status;
169
- const revision = document.createElement('span'); revision.textContent = 'revision ' + entry.revision; meta.append(kind, status, revision);
277
+ const status = document.createElement('span'); status.className = 'badge ' + entry.status; status.textContent = labelForStatus(entry.status);
278
+ const revision = document.createElement('span'); revision.textContent = t('revision') + ' ' + entry.revision; meta.append(kind, status, revision);
170
279
  const title = document.createElement('h3'); title.textContent = entry.title;
171
280
  const snippet = document.createElement('p'); snippet.className = 'snippet'; snippet.textContent = entry.summary || entry.body;
172
281
  const tags = document.createElement('div'); tags.className = 'tags'; entry.tags.forEach((tag) => { const item = document.createElement('button'); item.type = 'button'; item.className = 'tag'; item.textContent = '#' + tag; item.addEventListener('click', (event) => { event.stopPropagation(); state.tag = tag; renderFilters(); loadEntries(); }); tags.append(item); });
173
- const edit = document.createElement('button'); edit.type = 'button'; edit.className = 'button'; edit.textContent = '編集'; edit.style.marginTop = '12px'; edit.addEventListener('click', (event) => { event.stopPropagation(); selectEntry(entry.id); });
282
+ const edit = document.createElement('button'); edit.type = 'button'; edit.className = 'button'; setI18nText(edit, 'edit'); edit.style.marginTop = '12px'; edit.addEventListener('click', (event) => { event.stopPropagation(); selectEntry(entry.id); });
174
283
  card.append(meta, title, snippet, tags, edit); card.addEventListener('click', () => selectEntry(entry.id)); return card;
175
284
  }));
176
285
  }
@@ -178,27 +287,27 @@ export const WEB_HTML = String.raw `<!doctype html>
178
287
  function renderEditor() {
179
288
  const editor = $('editor');
180
289
  const entry = state.selected;
181
- $('editor-state').textContent = entry ? entry.status : '未選択';
182
- if (!entry) { const empty = document.createElement('div'); empty.className = 'editor-empty'; empty.textContent = '左の記憶を選択すると内容を編集できます。'; editor.replaceChildren(empty); return; }
290
+ updateEditorState();
291
+ if (!entry) { const empty = document.createElement('div'); empty.className = 'editor-empty'; setI18nText(empty, 'selectMemory'); editor.replaceChildren(empty); return; }
183
292
  const form = document.createElement('form'); form.className = 'form';
184
293
  const row = document.createElement('div'); row.className = 'form-row';
185
- const kindLabel = document.createElement('label'); kindLabel.textContent = '記憶タイプ'; const kind = document.createElement('select'); kinds.filter(([value]) => value !== 'all').forEach(([value, label]) => { const option = document.createElement('option'); option.value = value; option.textContent = label; kind.append(option); }); kind.value = entry.kind; kindLabel.append(kind);
186
- const statusLabel = document.createElement('label'); statusLabel.textContent = '状態'; const status = document.createElement('input'); status.value = entry.status; status.disabled = true; statusLabel.append(status); row.append(kindLabel, statusLabel);
187
- const titleLabel = document.createElement('label'); titleLabel.textContent = 'タイトル'; const title = document.createElement('input'); title.value = entry.title; titleLabel.append(title);
188
- const bodyLabel = document.createElement('label'); bodyLabel.textContent = '本文'; const body = document.createElement('textarea'); body.className = 'body'; body.value = entry.body; bodyLabel.append(body);
189
- const summaryLabel = document.createElement('label'); summaryLabel.textContent = '要約'; const summary = document.createElement('textarea'); summary.value = entry.summary || ''; summaryLabel.append(summary);
190
- const tagsLabel = document.createElement('label'); tagsLabel.textContent = 'タグ(カンマ区切り)'; const tags = document.createElement('input'); tags.value = entry.tags.join(', '); tagsLabel.append(tags);
294
+ const kindLabel = document.createElement('label'); const kindLabelText = document.createElement('span'); setI18nText(kindLabelText, 'memoryType'); const kind = document.createElement('select'); kinds.filter(([value]) => value !== 'all').forEach(([value, label]) => { const option = document.createElement('option'); option.value = value; setI18nText(option, label); kind.append(option); }); kind.value = entry.kind; kindLabel.append(kindLabelText, kind);
295
+ const statusLabel = document.createElement('label'); const statusLabelText = document.createElement('span'); setI18nText(statusLabelText, 'status'); const status = document.createElement('input'); status.value = labelForStatus(entry.status); status.dataset.i18nStatus = entry.status; status.disabled = true; statusLabel.append(statusLabelText, status); row.append(kindLabel, statusLabel);
296
+ const titleLabel = document.createElement('label'); const titleLabelText = document.createElement('span'); setI18nText(titleLabelText, 'title'); const title = document.createElement('input'); title.value = entry.title; titleLabel.append(titleLabelText, title);
297
+ const bodyLabel = document.createElement('label'); const bodyLabelText = document.createElement('span'); setI18nText(bodyLabelText, 'body'); const body = document.createElement('textarea'); body.className = 'body'; body.value = entry.body; bodyLabel.append(bodyLabelText, body);
298
+ const summaryLabel = document.createElement('label'); const summaryLabelText = document.createElement('span'); setI18nText(summaryLabelText, 'summary'); const summary = document.createElement('textarea'); summary.value = entry.summary || ''; summaryLabel.append(summaryLabelText, summary);
299
+ const tagsLabel = document.createElement('label'); const tagsLabelText = document.createElement('span'); setI18nText(tagsLabelText, 'commaSeparatedTags'); const tags = document.createElement('input'); tags.value = entry.tags.join(', '); tagsLabel.append(tagsLabelText, tags);
191
300
  const jsonRow = document.createElement('div'); jsonRow.className = 'form-row';
192
- const scopeLabel = document.createElement('label'); scopeLabel.textContent = 'scope JSON'; const scope = document.createElement('textarea'); scope.value = JSON.stringify(entry.scope, null, 2); scopeLabel.append(scope);
193
- const provenanceLabel = document.createElement('label'); provenanceLabel.textContent = 'provenance JSON'; const provenance = document.createElement('textarea'); provenance.value = JSON.stringify(entry.provenance, null, 2); provenanceLabel.append(provenance); jsonRow.append(scopeLabel, provenanceLabel);
301
+ const scopeLabel = document.createElement('label'); const scopeLabelText = document.createElement('span'); setI18nText(scopeLabelText, 'scopeJson'); const scope = document.createElement('textarea'); scope.value = JSON.stringify(entry.scope, null, 2); scopeLabel.append(scopeLabelText, scope);
302
+ const provenanceLabel = document.createElement('label'); const provenanceLabelText = document.createElement('span'); setI18nText(provenanceLabelText, 'provenanceJson'); const provenance = document.createElement('textarea'); provenance.value = JSON.stringify(entry.provenance, null, 2); provenanceLabel.append(provenanceLabelText, provenance); jsonRow.append(scopeLabel, provenanceLabel);
194
303
  const actions = document.createElement('div'); actions.className = 'toolbar';
195
- const save = document.createElement('button'); save.type = 'submit'; save.className = 'button primary'; save.textContent = '保存';
196
- const note = document.createElement('div'); note.className = entry.status === 'candidate' ? 'notice' : 'notice error'; note.textContent = entry.status === 'candidate' ? '候補エントリはrevisionを確認して更新します。' : 'verified / superseded は直接上書きできません。履歴を保つためCLIで置換してください。';
304
+ const save = document.createElement('button'); save.type = 'submit'; save.className = 'button primary'; setI18nText(save, 'save');
305
+ const note = document.createElement('div'); note.className = entry.status === 'candidate' ? 'notice' : 'notice error'; setI18nText(note, entry.status === 'candidate' ? 'candidateNotice' : 'immutableNotice');
197
306
  if (entry.status !== 'candidate') { save.disabled = true; [kind, title, body, summary, tags, scope, provenance].forEach((control) => { control.disabled = true; }); }
198
307
  actions.append(save); form.append(row, titleLabel, bodyLabel, summaryLabel, tagsLabel, jsonRow, note, actions); form.addEventListener('submit', async (event) => { event.preventDefault(); if (save.disabled) return; try {
199
308
  const payload = { expectedRevision: entry.revision, kind: kind.value, title: title.value, body: body.value, summary: summary.value || null, scope: JSON.parse(scope.value || '{}'), provenance: JSON.parse(provenance.value || '{}'), tags: escapeTags(tags.value) };
200
309
  await api('/api/entries/' + encodeURIComponent(entry.id) + '?workspace=' + encodeURIComponent(state.workspace), { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) });
201
- setStatus('保存しました。'); await loadEntries(); await selectEntry(entry.id);
310
+ setLocalizedStatus('saved'); await loadEntries(); await selectEntry(entry.id);
202
311
  } catch (error) { setStatus(error.message, true); }
203
312
  }); editor.replaceChildren(form);
204
313
  }
@@ -213,37 +322,37 @@ export const WEB_HTML = String.raw `<!doctype html>
213
322
  } catch (error) { setStatus(error.message, true); }
214
323
  }
215
324
 
216
- function detailBlock(title, value) {
325
+ function detailBlock(titleKey, value) {
217
326
  const block = document.createElement('section'); block.className = 'detail-block';
218
- const heading = document.createElement('h3'); heading.textContent = title;
327
+ const heading = document.createElement('h3'); setI18nText(heading, titleKey);
219
328
  const text = document.createElement('div'); text.className = 'detail-text'; text.textContent = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
220
329
  block.append(heading, text); return block;
221
330
  }
222
331
 
223
332
  function renderRunDetail() {
224
333
  const root = $('run-detail'); const detail = state.selectedRun;
225
- if (!detail) { const empty = document.createElement('div'); empty.className = 'editor-empty'; empty.textContent = 'runを選択すると詳細を表示します。'; root.replaceChildren(empty); return; }
226
- const heading = document.createElement('h3'); heading.textContent = 'Stored context / recommendations: untrusted, non-actionable';
334
+ if (!detail) { const empty = document.createElement('div'); empty.className = 'editor-empty'; setI18nText(empty, 'runSelect'); root.replaceChildren(empty); return; }
335
+ const heading = document.createElement('h3'); setI18nText(heading, 'contextWarning');
227
336
  const blocks = [
228
- detailBlock('run / intake', { run: detail.run, intake: detail.intake }),
229
- detailBlock('initial profile (immutable view)', detail.profile.initial),
230
- detailBlock('projected profile (ledger revisions)', detail.profile.projected),
231
- detailBlock('policy / profile source', { policyVersion: detail.profile.policyVersion, source: detail.profile.source, initialProfileHash: detail.profile.initialProfileHash }),
232
- detailBlock('coverage / evidence warnings', { coverage: detail.coverage, evidenceState: detail.evidenceState, warnings: detail.warnings }),
233
- detailBlock('timeline / evidence', { timeline: detail.timeline, evidence: detail.evidence }),
234
- detailBlock('context deliveries / selection reasons', detail.deliveries),
235
- detailBlock('context / run / intake feedback', detail.feedback),
236
- detailBlock('memory proposal links', { proposals: detail.proposals, links: detail.memoryLinks }),
337
+ detailBlock('detailRunIntake', { run: detail.run, intake: detail.intake }),
338
+ detailBlock('detailInitialProfile', detail.profile.initial),
339
+ detailBlock('detailProjectedProfile', detail.profile.projected),
340
+ detailBlock('detailPolicySource', { policyVersion: detail.profile.policyVersion, source: detail.profile.source, initialProfileHash: detail.profile.initialProfileHash }),
341
+ detailBlock('detailCoverageWarnings', { coverage: detail.coverage, evidenceState: detail.evidenceState, warnings: detail.warnings }),
342
+ detailBlock('detailTimelineEvidence', { timeline: detail.timeline, evidence: detail.evidence }),
343
+ detailBlock('detailDeliveriesReasons', detail.deliveries),
344
+ detailBlock('detailFeedback', detail.feedback),
345
+ detailBlock('detailProposalLinks', { proposals: detail.proposals, links: detail.memoryLinks }),
237
346
  ];
238
347
  root.replaceChildren(heading, ...blocks);
239
348
  }
240
349
 
241
350
  function renderRuns() {
242
351
  const root = $('run-list');
243
- if (!state.runs.length) { const empty = document.createElement('div'); empty.className = 'editor-empty'; empty.textContent = 'runはありません。'; root.replaceChildren(empty); return; }
352
+ if (!state.runs.length) { const empty = document.createElement('div'); empty.className = 'editor-empty'; setI18nText(empty, 'noRuns'); root.replaceChildren(empty); return; }
244
353
  root.replaceChildren(...state.runs.map((run) => {
245
354
  const button = document.createElement('button'); button.type = 'button'; button.className = 'run-card' + (state.selectedRun?.run?.runId === run.runId ? ' selected' : '');
246
- button.textContent = run.runId + ' / ' + (run.status || 'unknown') + ' / ' + (run.title || 'untitled');
355
+ button.textContent = run.runId + ' / ' + (run.status || t('unknown')) + ' / ' + (run.title || t('untitled'));
247
356
  button.addEventListener('click', () => loadRunDetail(run.runId)); return button;
248
357
  }));
249
358
  }
@@ -252,18 +361,40 @@ export const WEB_HTML = String.raw `<!doctype html>
252
361
  if (!state.workspace) return;
253
362
  try {
254
363
  const result = await api('/api/operator/runs?workspace=' + encodeURIComponent(state.workspace) + '&limit=50');
255
- state.runs = result.items || []; renderRuns(); $('run-page').textContent = result.nextCursor ? '次ページあり(bounded cursor)' : '末尾';
364
+ state.runs = result.items || []; renderRuns(); setI18nText($('run-page'), result.nextCursor ? 'nextPage' : 'end');
256
365
  if (!state.selectedRun && state.runs[0]) await loadRunDetail(state.runs[0].runId);
257
366
  else renderRunDetail();
258
367
  } catch (error) { setStatus(error.message, true); }
259
368
  }
260
369
 
261
- async function loadEntries() { if (!state.workspace) return; try { const params = new URLSearchParams({ workspace: state.workspace }); if (state.kind !== 'all') params.set('kind', state.kind); if (state.tag) params.set('tag', state.tag); if (state.query) params.set('q', state.query); const result = await api('/api/entries?' + params); state.entries = result.entries; if (state.selected && !state.entries.some((entry) => entry.id === state.selected.id)) state.selected = null; renderEntries(); renderFilters(); if (!state.selected && state.entries[0]) await selectEntry(state.entries[0].id); else renderEditor(); setStatus(String(result.entries.length) + '件を表示中'); } catch (error) { setStatus(error.message, true); } }
370
+ async function loadEntries() { if (!state.workspace) return; try { const params = new URLSearchParams({ workspace: state.workspace }); if (state.kind !== 'all') params.set('kind', state.kind); if (state.tag) params.set('tag', state.tag); if (state.query) params.set('q', state.query); const result = await api('/api/entries?' + params); state.entries = result.entries; if (state.selected && !state.entries.some((entry) => entry.id === state.selected.id)) state.selected = null; renderEntries(); renderFilters(); if (!state.selected && state.entries[0]) await selectEntry(state.entries[0].id); else renderEditor(); setLocalizedCountStatus('displayedCount', result.entries.length); } catch (error) { setStatus(error.message, true); } }
262
371
  async function loadTags() { if (!state.workspace) return; try { const result = await api('/api/tags?workspace=' + encodeURIComponent(state.workspace)); state.tags = result.tags; renderFilters(); } catch (error) { setStatus(error.message, true); } }
263
- async function loadWorkspaces() { try { const result = await api('/api/workspaces'); const select = $('workspace'); select.replaceChildren(...result.workspaces.map((item) => { const option = document.createElement('option'); option.value = item.workspace; option.textContent = (item.displayName || item.workspace) + ' (' + item.count + ')'; return option; })); if (!state.workspace && result.workspaces[0]) state.workspace = result.workspaces[0].workspace; select.value = state.workspace; if (state.workspace) { await loadTags(); await loadEntries(); await loadRuns(); } else setStatus('workspaceがありません。CLIで kiokuko use または record を実行してください。', true); } catch (error) { setStatus(error.message, true); } }
372
+ async function loadWorkspaces() { try { const result = await api('/api/workspaces'); const select = $('workspace'); select.replaceChildren(...result.workspaces.map((item) => { const option = document.createElement('option'); option.value = item.workspace; option.textContent = (item.displayName || item.workspace) + ' (' + item.count + ')'; return option; })); if (!state.workspace && result.workspaces[0]) state.workspace = result.workspaces[0].workspace; select.value = state.workspace; if (state.workspace) { await loadTags(); await loadEntries(); await loadRuns(); } else setLocalizedStatus('noWorkspace', {}, true); } catch (error) { setStatus(error.message, true); } }
373
+ $('language-toggle').addEventListener('click', () => {
374
+ setLanguageMenuOpen($('language-toggle').getAttribute('aria-expanded') !== 'true');
375
+ });
376
+ $('language-toggle').addEventListener('keydown', (event) => {
377
+ if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
378
+ event.preventDefault();
379
+ setLanguageMenuOpen(true);
380
+ });
381
+ $('language-menu').addEventListener('keydown', (event) => {
382
+ const options = [...$('language-menu').querySelectorAll('.language-option')];
383
+ const current = options.indexOf(document.activeElement);
384
+ let next = null;
385
+ if (event.key === 'ArrowDown') next = options[(current + 1) % options.length];
386
+ if (event.key === 'ArrowUp') next = options[(current - 1 + options.length) % options.length];
387
+ if (event.key === 'Home') next = options[0];
388
+ if (event.key === 'End') next = options.at(-1);
389
+ if (event.key === 'Escape') { event.preventDefault(); setLanguageMenuOpen(false, true); return; }
390
+ if (next) { event.preventDefault(); next.focus(); }
391
+ });
392
+ document.addEventListener('click', (event) => { if (!$('language-picker')?.contains(event.target)) setLanguageMenuOpen(false); });
393
+ document.addEventListener('focusin', (event) => { if (!$('language-picker')?.contains(event.target)) setLanguageMenuOpen(false); });
264
394
  $('workspace').addEventListener('change', (event) => { state.workspace = event.target.value; state.selected = null; state.selectedRun = null; state.runs = []; state.tag = ''; loadTags().then(loadEntries).then(loadRuns); });
265
395
  $('refresh').addEventListener('click', () => loadWorkspaces());
266
396
  let searchTimer; $('search').addEventListener('input', (event) => { clearTimeout(searchTimer); state.query = event.target.value.trim(); searchTimer = setTimeout(() => loadEntries(), 180); });
397
+ applyTranslations();
267
398
  loadWorkspaces();
268
399
  </script>
269
400
  </body>
@@ -1 +1 @@
1
- {"version":3,"file":"ui.js","sourceRoot":"","sources":["../../src/web/ui.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA6Q1B,CAAC"}
1
+ {"version":3,"file":"ui.js","sourceRoot":"","sources":["../../src/web/ui.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE7F,MAAM,eAAe,GAAG,IAAI,CAAC,SAAS,CAAC;IACrC,aAAa,EAAE,kBAAkB;IACjC,YAAY,EAAE,iBAAiB;IAC/B,OAAO,EAAE,WAAW;IACpB,QAAQ,EAAE,YAAY;CACvB,CAAC;KACC,UAAU,CAAC,GAAG,EAAE,SAAS,CAAC;KAC1B,UAAU,CAAC,GAAG,EAAE,SAAS,CAAC;KAC1B,UAAU,CAAC,GAAG,EAAE,SAAS,CAAC;KAC1B,UAAU,CAAC,QAAQ,EAAE,SAAS,CAAC;KAC/B,UAAU,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAEnC,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAmIf,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAiQ1B,CAAC"}
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@askdkc/kiokuko",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Model-agnostic external memory for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "kiokuko": "dist/bin/kiokuko.js"
8
8
  },
9
9
  "engines": {
10
- "node": ">=22.16.0"
10
+ "node": ">=24.0.0"
11
11
  },
12
12
  "publishConfig": {
13
13
  "access": "public"
@@ -38,7 +38,7 @@
38
38
  "zod": "^4.4.3"
39
39
  },
40
40
  "devDependencies": {
41
- "@types/node": "^22.15.0",
41
+ "@types/node": "^24.0.0",
42
42
  "tsx": "^4.20.3",
43
43
  "typescript": "^5.9.2"
44
44
  }
@@ -1,5 +1,5 @@
1
1
  <!-- BEGIN KIOKUKO MANAGED BLOCK -->
2
- <!-- kiokuko-template-version: 4 -->
2
+ <!-- kiokuko-template-version: 5 -->
3
3
  <!-- This section is managed by `kiokuko use`. Edit outside the markers. -->
4
4
 
5
5
  ## Kiokuko external memory
@@ -14,7 +14,7 @@ Use the Kiokuko MCP tools rather than reading or modifying the SQLite file direc
14
14
 
15
15
  ### Before non-trivial work
16
16
 
17
- 1. Call `task_prepare` with the actual task, current working directory, and only profile hints supported by the user request or repository evidence.
17
+ 1. Call `task_prepare` at most once for the current user request, with the actual task, current working directory, and only profile hints supported by the user request or repository evidence. Reuse its result for the rest of the request; never call it again after `memory_checkpoint`.
18
18
  2. Include the complete names and short descriptions of skills and MCP tools available in the current client. Pass an empty catalog only when none are available; omit it when availability is unknown. The catalog is not stored.
19
19
  3. Kiokuko may consult `https://github.com/mattpocock/skills` only when the supplied catalog contains zero skills. If any skill is available, or the catalog is unknown, external skill fallback stays disabled.
20
20
  4. If the intake needs an answer, call `task_answer` with the same capability catalog only when current evidence supports the answer; otherwise ask the user the returned question.
@@ -23,9 +23,11 @@ Use the Kiokuko MCP tools rather than reading or modifying the SQLite file direc
23
23
 
24
24
  ### After substantial work
25
25
 
26
- 1. Call `memory_checkpoint` only for concise, durable, verified facts, decisions, lessons, preferences, or references that will help future work.
27
- 2. Keep repository knowledge in project scope. Use global scope only for knowledge that truly applies across projects.
28
- 3. Checkpoints remain untrusted candidates until explicitly reviewed; never auto-promote them to verified.
26
+ 1. Call `memory_checkpoint` at most once for the current user request, only for concise, durable, verified facts, decisions, lessons, preferences, or references that will help future work.
27
+ 2. Treat a completed `memory_checkpoint` as terminal for tool use: do not call it or any other tool again; immediately return the final response.
28
+ 3. Do not retry an unchanged tool call after it fails or returns no new information. Summarize the blocker or current result and stop tool use.
29
+ 4. Keep repository knowledge in project scope. Use global scope only for knowledge that truly applies across projects.
30
+ 5. Checkpoints remain untrusted candidates until explicitly reviewed; never auto-promote them to verified.
29
31
 
30
32
  If the MCP tools are unavailable, report the failure briefly and continue from repository evidence. Never store passwords, API keys, access tokens, private keys, session cookies, auth headers, provider credentials, client secrets, private user data, full transcripts, or capability catalogs.
31
33