@shadowforge0/aquifer-memory 1.5.12 → 1.7.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 +23 -0
- package/README.md +84 -73
- package/README_CN.md +676 -0
- package/README_TW.md +684 -0
- package/aquifer.config.example.json +34 -0
- package/consumers/claude-code.js +11 -11
- package/consumers/cli.js +421 -53
- package/consumers/codex-handoff.js +258 -0
- package/consumers/codex.js +1676 -0
- package/consumers/default/daily-entries.js +23 -4
- package/consumers/default/index.js +2 -2
- package/consumers/default/prompts/summary.js +6 -6
- package/consumers/mcp.js +96 -5
- package/consumers/openclaw-ext/index.js +0 -1
- package/consumers/openclaw-plugin.js +1 -1
- package/consumers/shared/config.js +8 -0
- package/consumers/shared/factory.js +1 -0
- package/consumers/shared/ingest.js +1 -1
- package/consumers/shared/normalize.js +14 -3
- package/consumers/shared/recall-format.js +27 -0
- package/consumers/shared/summary-parser.js +151 -0
- package/core/aquifer.js +380 -18
- package/core/finalization-review.js +319 -0
- package/core/mcp-manifest.js +52 -2
- package/core/memory-bootstrap.js +200 -0
- package/core/memory-consolidation.js +1590 -0
- package/core/memory-promotion.js +544 -0
- package/core/memory-recall.js +247 -0
- package/core/memory-records.js +797 -0
- package/core/memory-safety-gate.js +224 -0
- package/core/session-finalization.js +365 -0
- package/core/storage.js +385 -2
- package/docs/getting-started.md +105 -0
- package/docs/postprocess-contract.md +2 -2
- package/docs/setup.md +92 -2
- package/package.json +25 -11
- package/pipeline/normalize/adapters/codex.js +106 -0
- package/pipeline/normalize/detect.js +3 -2
- package/schema/001-base.sql +3 -0
- package/schema/007-v1-foundation.sql +273 -0
- package/schema/008-session-finalizations.sql +50 -0
- package/schema/009-v1-assertion-plane.sql +193 -0
- package/schema/010-v1-finalization-review.sql +160 -0
- package/schema/011-v1-compaction-claim.sql +46 -0
- package/schema/012-v1-compaction-lease.sql +39 -0
- package/schema/013-v1-compaction-lineage.sql +193 -0
- package/scripts/codex-recovery.js +672 -0
- package/consumers/miranda/context-inject.js +0 -120
- package/consumers/miranda/daily-entries.js +0 -224
- package/consumers/miranda/index.js +0 -364
- package/consumers/miranda/instance.js +0 -55
- package/consumers/miranda/llm.js +0 -99
- package/consumers/miranda/profile.json +0 -145
- package/consumers/miranda/prompts/summary.js +0 -303
- package/consumers/miranda/recall-format.js +0 -76
- package/consumers/miranda/render-daily-md.js +0 -186
- package/consumers/miranda/workspace-files.js +0 -91
- package/scripts/drop-entity-state-history.sql +0 -17
- package/scripts/drop-insights.sql +0 -12
- package/scripts/install-openclaw.sh +0 -59
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { getDailyEntries } = require('./daily-entries');
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// buildSessionContext — pure function, testable.
|
|
7
|
-
// Emits the Miranda persona briefing that gets prepended to the system
|
|
8
|
-
// prompt. Style: 散文段落, 結論收尾, 不給 bullet/table/headers.
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
|
|
11
|
-
const TODO_CAP = 5;
|
|
12
|
-
|
|
13
|
-
function buildSessionContext({ today, agentId, focusText, todoItems, moodLine, handoffText, cliEntries }) {
|
|
14
|
-
const parts = [];
|
|
15
|
-
parts.push('你是 Miranda。以下是你已經知道的現況,直接用來回應,不需要讀檔或搜尋。像做 briefing——帶現況也帶判斷和建議。用散文段落,最後一句必須是結論或建議,不能是問句。若草稿有 bullet、標題、表格或問句收尾,改寫再送出。');
|
|
16
|
-
parts.push('回答任何關於過去做過什麼、討論過什麼、決策過什麼的問題時,第一步用 session_recall MCP tool 查,不要用 grep、讀 log、翻檔案。工具在手上就用。');
|
|
17
|
-
parts.push('用完 session_recall 後,如果某筆結果實際幫助了你的回答,呼叫 session_feedback(sessionId, verdict="helpful");如果結果明顯過時或錯誤,呼叫 session_feedback(sessionId, verdict="unhelpful"),帶簡短 note 說明原因。只對實際影響回答的結果回饋,不要每次 recall 都打分。');
|
|
18
|
-
|
|
19
|
-
if (focusText) parts.push(`現在的焦點是${focusText}。`);
|
|
20
|
-
if (handoffText) parts.push(`上一段的交接:${handoffText}`);
|
|
21
|
-
|
|
22
|
-
const items = (todoItems || []).slice(0, TODO_CAP);
|
|
23
|
-
if (items.length > 0) parts.push(`手上還有${items.join('、')}。`);
|
|
24
|
-
|
|
25
|
-
if (moodLine) parts.push(`整體狀態${moodLine}。`);
|
|
26
|
-
|
|
27
|
-
const cli = (cliEntries || []).slice(-15);
|
|
28
|
-
if (cli.length > 0) parts.push(`今天已經做過的事(不要重複):${cli.join(';')}`);
|
|
29
|
-
|
|
30
|
-
if (parts.length <= 2) return '';
|
|
31
|
-
return `<session-context date="${today}" agent="${agentId}">\n${parts.join('\n')}\n</session-context>`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
// extractFocusTodoMood — pull state rows (focus/todo/mood/handoff) + cli log
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
|
|
38
|
-
function extractFocusTodoMood(todayEntries, yesterdayEntries) {
|
|
39
|
-
const allEntries = [...(todayEntries || []), ...(yesterdayEntries || [])]
|
|
40
|
-
.sort((a, b) => new Date(b.event_at) - new Date(a.event_at));
|
|
41
|
-
|
|
42
|
-
const preferCli = (tag) => {
|
|
43
|
-
const cli = allEntries.find(e => e.tag === tag && e.source === 'cli');
|
|
44
|
-
return cli || allEntries.find(e => e.tag === tag);
|
|
45
|
-
};
|
|
46
|
-
const focusEntry = preferCli('[FOCUS]');
|
|
47
|
-
const todoEntry = preferCli('[TODO]');
|
|
48
|
-
const moodEntry = allEntries.find(e => e.tag === '[MOOD]');
|
|
49
|
-
const handoffEntry = preferCli('[HANDOFF]');
|
|
50
|
-
|
|
51
|
-
const focusText = focusEntry
|
|
52
|
-
? focusEntry.text.split('\n').map(l => l.trim().replace(/^-\s*/, '')).filter(Boolean).join(', ')
|
|
53
|
-
: '';
|
|
54
|
-
|
|
55
|
-
const todoItems = todoEntry
|
|
56
|
-
? todoEntry.text.split('\n').map(l => l.trim().replace(/^-\s*/, '')).filter(Boolean)
|
|
57
|
-
: [];
|
|
58
|
-
|
|
59
|
-
const moodLine = moodEntry ? moodEntry.text.trim() : '';
|
|
60
|
-
|
|
61
|
-
let handoffText = '';
|
|
62
|
-
if (handoffEntry) {
|
|
63
|
-
const meta = handoffEntry.metadata || {};
|
|
64
|
-
const isTrivial = (meta.status === 'completed' && meta.next === '無')
|
|
65
|
-
|| handoffEntry.text.trim().startsWith('上一段已完成 簡短交談');
|
|
66
|
-
if (!isTrivial) {
|
|
67
|
-
handoffText = handoffEntry.text.trim();
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const stateTags = new Set(['[FOCUS]', '[TODO]', '[MOOD]', '[HANDOFF]', '[HIGHLIGHT]', '[NARRATIVE]']);
|
|
72
|
-
const logEntries = (todayEntries || [])
|
|
73
|
-
.filter(e => !stateTags.has(e.tag))
|
|
74
|
-
.sort((a, b) => new Date(a.event_at) - new Date(b.event_at))
|
|
75
|
-
.map(e => e.text.trim())
|
|
76
|
-
.filter(Boolean);
|
|
77
|
-
|
|
78
|
-
return { focusText, todoItems, moodLine, handoffText, cliEntries: logEntries };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
|
-
// computeInjection — gateway/CC shared: queries daily_entries + aquifer.bootstrap
|
|
83
|
-
// and returns a ready-to-prepend context string. No host-specific wiring.
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
|
-
|
|
86
|
-
function dateTaipei(d) {
|
|
87
|
-
return new Intl.DateTimeFormat('sv-SE', {
|
|
88
|
-
timeZone: 'Asia/Taipei', year: 'numeric', month: '2-digit', day: '2-digit',
|
|
89
|
-
}).format(d);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function computeInjection({ aquifer, pool, agentId, now, includeBootstrap = true }) {
|
|
93
|
-
if (!now) now = new Date();
|
|
94
|
-
const today = dateTaipei(now);
|
|
95
|
-
const yesterday = dateTaipei(new Date(now.getTime() - 86400000));
|
|
96
|
-
|
|
97
|
-
const [todayEntries, yesterdayEntries] = await Promise.all([
|
|
98
|
-
getDailyEntries(pool, today, agentId),
|
|
99
|
-
getDailyEntries(pool, yesterday, agentId),
|
|
100
|
-
]);
|
|
101
|
-
|
|
102
|
-
const state = extractFocusTodoMood(todayEntries, yesterdayEntries);
|
|
103
|
-
const context = buildSessionContext({ today, agentId, ...state });
|
|
104
|
-
|
|
105
|
-
let bootstrapText = '';
|
|
106
|
-
if (includeBootstrap && aquifer) {
|
|
107
|
-
try {
|
|
108
|
-
const bs = await aquifer.bootstrap({ agentId, limit: 5, maxChars: 2000, format: 'text' });
|
|
109
|
-
if (bs.text && bs.sessions && bs.sessions.length > 0) bootstrapText = '\n' + bs.text;
|
|
110
|
-
} catch { /* best-effort */ }
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return context + bootstrapText;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
module.exports = {
|
|
117
|
-
buildSessionContext,
|
|
118
|
-
extractFocusTodoMood,
|
|
119
|
-
computeInjection,
|
|
120
|
-
};
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const crypto = require('crypto');
|
|
4
|
-
const { parseHandoffSection } = require('./prompts/summary');
|
|
5
|
-
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Miranda daily log — writes to host-owned `miranda.daily_entries` table.
|
|
8
|
-
//
|
|
9
|
-
// Self-contained DAL: the SQL lives here instead of in session-dal so the
|
|
10
|
-
// plugin doesn't pull in OpenClaw host code. The host injects a pg.Pool.
|
|
11
|
-
//
|
|
12
|
-
// Table layout (matches live miranda.daily_entries):
|
|
13
|
-
// id, event_at, source, tag, text, agent_id, session_id, metadata, dedupe_key
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
|
|
16
|
-
const TABLE = 'miranda.daily_entries';
|
|
17
|
-
const UPSERT_TAGS = new Set(['[FOCUS]', '[TODO]', '[STATS]', '[HIGHLIGHT]', '[SYSTEM]', '[HANDOFF]']);
|
|
18
|
-
|
|
19
|
-
function taipeiDateString(now) {
|
|
20
|
-
if (!now) now = new Date();
|
|
21
|
-
return new Intl.DateTimeFormat('sv-SE', {
|
|
22
|
-
timeZone: 'Asia/Taipei',
|
|
23
|
-
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
24
|
-
}).format(now);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function textHash6(text) {
|
|
28
|
-
const normalized = (text || '').normalize('NFKC').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
29
|
-
if (!normalized) return 'empty';
|
|
30
|
-
return crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 6);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function insertDailyEntry(pool, { eventAt, source, tag, text, agentId, sessionId, metadata, dedupeKey }) {
|
|
34
|
-
const shouldUpsert = dedupeKey && UPSERT_TAGS.has(tag);
|
|
35
|
-
const sql = shouldUpsert
|
|
36
|
-
? `INSERT INTO ${TABLE}
|
|
37
|
-
(event_at, source, tag, text, agent_id, session_id, metadata, dedupe_key)
|
|
38
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
39
|
-
ON CONFLICT (dedupe_key) DO UPDATE SET
|
|
40
|
-
text = EXCLUDED.text,
|
|
41
|
-
event_at = EXCLUDED.event_at,
|
|
42
|
-
metadata = EXCLUDED.metadata
|
|
43
|
-
RETURNING id, event_at, source, tag, text`
|
|
44
|
-
: `INSERT INTO ${TABLE}
|
|
45
|
-
(event_at, source, tag, text, agent_id, session_id, metadata, dedupe_key)
|
|
46
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
47
|
-
ON CONFLICT (dedupe_key) DO NOTHING
|
|
48
|
-
RETURNING id, event_at, source, tag, text`;
|
|
49
|
-
const result = await pool.query(sql, [
|
|
50
|
-
eventAt,
|
|
51
|
-
source,
|
|
52
|
-
tag || null,
|
|
53
|
-
text,
|
|
54
|
-
agentId || 'main',
|
|
55
|
-
sessionId || null,
|
|
56
|
-
metadata ? JSON.stringify(metadata) : '{}',
|
|
57
|
-
dedupeKey || null,
|
|
58
|
-
]);
|
|
59
|
-
return result.rows[0] || null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async function getDailyEntries(pool, date, agentId) {
|
|
63
|
-
const result = await pool.query(
|
|
64
|
-
`SELECT * FROM ${TABLE}
|
|
65
|
-
WHERE (event_at AT TIME ZONE 'Asia/Taipei')::date = $1
|
|
66
|
-
AND ($2::text IS NULL OR agent_id = $2)
|
|
67
|
-
ORDER BY event_at ASC`,
|
|
68
|
-
[date, agentId || null],
|
|
69
|
-
);
|
|
70
|
-
return result.rows;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
|
|
75
|
-
async function fetchDailyContext(pool, date, agentId) {
|
|
76
|
-
const rows = await getDailyEntries(pool, date, agentId);
|
|
77
|
-
if (!rows || rows.length === 0) return '';
|
|
78
|
-
|
|
79
|
-
let currentFocus = '';
|
|
80
|
-
let currentTodo = '';
|
|
81
|
-
const entries = [];
|
|
82
|
-
|
|
83
|
-
for (const row of rows) {
|
|
84
|
-
if (row.tag === '[FOCUS]') currentFocus = row.text;
|
|
85
|
-
else if (row.tag === '[TODO]') currentTodo = row.text;
|
|
86
|
-
else if (!row.tag || row.tag === '[CLI]') {
|
|
87
|
-
const time = new Date(row.event_at).toLocaleTimeString('sv-SE', {
|
|
88
|
-
timeZone: 'Asia/Taipei', hour: '2-digit', minute: '2-digit',
|
|
89
|
-
});
|
|
90
|
-
entries.push(`- (${time}) ${row.text}`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const recentEntries = entries.slice(-20);
|
|
95
|
-
const parts = [];
|
|
96
|
-
if (currentFocus) parts.push(`當前焦點: ${currentFocus}`);
|
|
97
|
-
if (currentTodo) parts.push(`當前待辦:\n${currentTodo}`);
|
|
98
|
-
if (recentEntries.length > 0) parts.push(`今日紀錄:\n${recentEntries.join('\n')}`);
|
|
99
|
-
|
|
100
|
-
let text = parts.join('\n\n');
|
|
101
|
-
if (text.length > 3000) text = text.slice(0, 3000) + '\n...(truncated)';
|
|
102
|
-
return text;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function writeDailyEntries({
|
|
106
|
-
sections, recap, pool, sessionId, agentId, logger = console,
|
|
107
|
-
source = 'afterburn', tag = null, now, renderDailyLog,
|
|
108
|
-
}) {
|
|
109
|
-
if (!now) now = new Date();
|
|
110
|
-
// Daily entries are work logs — always attribute to main
|
|
111
|
-
if (agentId === 'cc') agentId = 'main';
|
|
112
|
-
const date = taipeiDateString(now);
|
|
113
|
-
let inserted = 0;
|
|
114
|
-
let focusUpdated = false;
|
|
115
|
-
let todoUpdated = false;
|
|
116
|
-
|
|
117
|
-
// Session bullets
|
|
118
|
-
if (sections?.session_entries) {
|
|
119
|
-
const entryLines = sections.session_entries.split('\n');
|
|
120
|
-
const bullets = entryLines.filter(l => l.trim().startsWith('- ')).map(l => l.trim().slice(2));
|
|
121
|
-
for (const bullet of bullets) {
|
|
122
|
-
const timeMatch = bullet.match(/^\((\d{2}:\d{2})\)\s*(.*)/);
|
|
123
|
-
const text = timeMatch ? timeMatch[2] : bullet;
|
|
124
|
-
const eventAt = now.toISOString();
|
|
125
|
-
const row = await insertDailyEntry(pool, {
|
|
126
|
-
eventAt, source, tag, text,
|
|
127
|
-
agentId, sessionId, metadata: {},
|
|
128
|
-
dedupeKey: `daily:${date}:${textHash6(text)}`,
|
|
129
|
-
});
|
|
130
|
-
if (row) inserted++;
|
|
131
|
-
}
|
|
132
|
-
if (logger.info) logger.info(`[miranda] wrote ${inserted} daily entries`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Focus
|
|
136
|
-
if (recap?.focus_decision === 'update' && recap.focus) {
|
|
137
|
-
await insertDailyEntry(pool, {
|
|
138
|
-
eventAt: now.toISOString(), source, tag: '[FOCUS]',
|
|
139
|
-
text: recap.focus, agentId, sessionId,
|
|
140
|
-
metadata: { proposed_by: source },
|
|
141
|
-
dedupeKey: `daily:${date}:focus:${source}`,
|
|
142
|
-
});
|
|
143
|
-
focusUpdated = true;
|
|
144
|
-
if (logger.info) logger.info(`[miranda] focus updated: ${recap.focus.slice(0, 60)}`);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// TODO
|
|
148
|
-
if (recap?.todo_new?.length > 0 || recap?.todo_done?.length > 0) {
|
|
149
|
-
const todayEntries = await getDailyEntries(pool, date, agentId);
|
|
150
|
-
let currentItems = [];
|
|
151
|
-
for (const row of todayEntries) {
|
|
152
|
-
if (row.tag === '[TODO]') {
|
|
153
|
-
currentItems = row.text.split('\n').map(s => s.replace(/^[-•]\s*/, '').trim()).filter(Boolean);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
if (recap.todo_done?.length > 0) {
|
|
157
|
-
for (const done of recap.todo_done) {
|
|
158
|
-
const dl = done.toLowerCase();
|
|
159
|
-
currentItems = currentItems.filter(item => {
|
|
160
|
-
const il = item.toLowerCase();
|
|
161
|
-
return il !== dl && !il.includes(dl) && !dl.includes(il);
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
if (recap.todo_new?.length > 0) {
|
|
166
|
-
for (const n of recap.todo_new) {
|
|
167
|
-
if (!currentItems.some(i => i.toLowerCase() === n.toLowerCase())) currentItems.push(n);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
await insertDailyEntry(pool, {
|
|
171
|
-
eventAt: now.toISOString(), source, tag: '[TODO]',
|
|
172
|
-
text: currentItems.map(i => `- ${i}`).join('\n') || '(全部完成)',
|
|
173
|
-
agentId, sessionId,
|
|
174
|
-
metadata: { proposed_by: source, todo_new: recap.todo_new, todo_done: recap.todo_done },
|
|
175
|
-
dedupeKey: `daily:${date}:todo:${source}`,
|
|
176
|
-
});
|
|
177
|
-
todoUpdated = true;
|
|
178
|
-
if (logger.info) logger.info(`[miranda] todo updated: ${currentItems.length} items`);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Handoff
|
|
182
|
-
if (sections?.handoff) {
|
|
183
|
-
const handoff = parseHandoffSection(sections.handoff);
|
|
184
|
-
if (handoff) {
|
|
185
|
-
let handoffText;
|
|
186
|
-
switch (handoff.status) {
|
|
187
|
-
case 'completed': handoffText = `上一段已完成 ${handoff.lastStep}`; break;
|
|
188
|
-
case 'blocked': handoffText = `上一段卡在 ${handoff.lastStep}`; break;
|
|
189
|
-
default: handoffText = `上一段停在 ${handoff.lastStep}`;
|
|
190
|
-
}
|
|
191
|
-
if (handoff.next && handoff.next !== '無') handoffText += `,下一步建議 ${handoff.next}`;
|
|
192
|
-
if (handoff.decided) handoffText += `,已決定 ${handoff.decided}`;
|
|
193
|
-
if (handoff.blocker && handoff.status !== 'blocked') handoffText += `,卡在 ${handoff.blocker}`;
|
|
194
|
-
handoffText += '。';
|
|
195
|
-
|
|
196
|
-
await insertDailyEntry(pool, {
|
|
197
|
-
eventAt: now.toISOString(), source, tag: '[HANDOFF]',
|
|
198
|
-
text: handoffText, agentId, sessionId,
|
|
199
|
-
metadata: { ...handoff, proposed_by: source },
|
|
200
|
-
dedupeKey: `daily:${date}:handoff:${source}`,
|
|
201
|
-
});
|
|
202
|
-
if (logger.info) logger.info(`[miranda] handoff written: ${handoffText.slice(0, 80)}`);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Optional custom renderer (persona can plug in a markdown writer)
|
|
207
|
-
if (renderDailyLog) {
|
|
208
|
-
try { await renderDailyLog(date, agentId); }
|
|
209
|
-
catch (err) { if (logger.info) logger.info(`[miranda] renderDailyLog failed: ${err.message}`); }
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return { inserted, focusUpdated, todoUpdated };
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
module.exports = {
|
|
216
|
-
taipeiDateString,
|
|
217
|
-
textHash6,
|
|
218
|
-
insertDailyEntry,
|
|
219
|
-
getDailyEntries,
|
|
220
|
-
fetchDailyContext,
|
|
221
|
-
writeDailyEntries,
|
|
222
|
-
TABLE,
|
|
223
|
-
UPSERT_TAGS,
|
|
224
|
-
};
|