mixdog 0.9.28 → 0.9.29
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/package.json +1 -1
- package/src/rules/lead/lead-tool.md +1 -2
- package/src/rules/shared/01-tool.md +13 -11
- package/src/runtime/channels/backends/discord-access.mjs +1 -3
- package/src/runtime/channels/backends/telegram.mjs +1 -1
- package/src/runtime/channels/lib/config.mjs +5 -29
- package/src/runtime/shared/config.mjs +1 -6
- package/src/runtime/shared/schedules-db.mjs +2 -120
- package/src/runtime/shared/webhooks-db.mjs +3 -138
- package/src/standalone/channel-admin.mjs +6 -27
- package/src/tui/display-width.mjs +4 -1
- package/src/tui/dist/index.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +10 -10
- package/vendor/ink/build/display-width.js +3 -1
package/package.json
CHANGED
|
@@ -5,5 +5,4 @@
|
|
|
5
5
|
directly.
|
|
6
6
|
- Use the session's current project/workspace. Change the work project only
|
|
7
7
|
when the user asks for another project or a tool call needs another root.
|
|
8
|
-
- Use `agent` for scoped implementation, research, review, and debugging
|
|
9
|
-
for git commit/push/stash or Ship.
|
|
8
|
+
- Use `agent` for scoped implementation, research, review, and debugging.
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# Tool Use
|
|
2
2
|
|
|
3
|
-
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
- BATCH OR IT IS A DEFECT — this is the single hardest rule here. Every turn
|
|
4
|
+
fires ALL independent calls at once; a second consecutive single-lookup,
|
|
5
|
+
single-`shell`, or single-`apply_patch` turn is a defect, never a style
|
|
6
|
+
choice. Serialize ONLY on a real data dependency — nothing else earns a
|
|
7
|
+
solo call. Merge variants/scopes into ONE call wherever the schema takes
|
|
8
|
+
arrays (`pattern[]`, `path[]`, `symbols[]`, `query[]`), chain shell with
|
|
9
|
+
`;`/`&&` or run them in parallel, and put every known edit in ONE patch.
|
|
8
10
|
- Route by what is already known: known symbol/relation → `code_graph`;
|
|
9
11
|
exact text in a known scope → `grep`; unknown location, machine-wide/
|
|
10
12
|
out-of-repo whereabouts, or concept-level question → `explore` (which uses
|
|
@@ -33,9 +35,9 @@
|
|
|
33
35
|
unit (function/section) — over-read once instead of paging the same file
|
|
34
36
|
in small windows across turns; a 3rd window into one file means the first
|
|
35
37
|
should have been wider.
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
- Parallel-first has ONE carve-out: verifying your own edits. Keep an
|
|
39
|
+
`apply_patch` and the `shell` that checks it in separate turns — not for
|
|
40
|
+
ordering (same-turn calls run in emit order), but because you must SEE the
|
|
41
|
+
patch result before verifying: a same-turn shell is already emitted and
|
|
42
|
+
can't react to a failed or partial patch. Batch the patches this turn,
|
|
43
|
+
verify them all in ONE shell call the next. Anything else stays parallel.
|
|
@@ -11,9 +11,7 @@ function defaultAccess() {
|
|
|
11
11
|
function normalizeAccess(parsed) {
|
|
12
12
|
const defaults = defaultAccess();
|
|
13
13
|
return {
|
|
14
|
-
|
|
15
|
-
// completable); normalize it to "allowlist" on load.
|
|
16
|
-
dmPolicy: parsed?.dmPolicy === "pairing" ? "allowlist" : (parsed?.dmPolicy ?? defaults.dmPolicy),
|
|
14
|
+
dmPolicy: parsed?.dmPolicy ?? defaults.dmPolicy,
|
|
17
15
|
allowFrom: parsed?.allowFrom ?? defaults.allowFrom,
|
|
18
16
|
channels: parsed?.channels ?? defaults.channels,
|
|
19
17
|
mentionPatterns: parsed?.mentionPatterns,
|
|
@@ -56,7 +56,7 @@ function defaultAccess() {
|
|
|
56
56
|
function normalizeAccess(parsed) {
|
|
57
57
|
const defaults = defaultAccess();
|
|
58
58
|
return {
|
|
59
|
-
dmPolicy: parsed?.dmPolicy
|
|
59
|
+
dmPolicy: parsed?.dmPolicy ?? defaults.dmPolicy,
|
|
60
60
|
allowFrom: parsed?.allowFrom ?? defaults.allowFrom,
|
|
61
61
|
channels: parsed?.channels ?? defaults.channels,
|
|
62
62
|
mentionPatterns: parsed?.mentionPatterns,
|
|
@@ -39,27 +39,10 @@ function channelIdForBackend(entry = {}, backend = "discord") {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// Resolve the single active-backend channel id from the config's `channel`
|
|
42
|
-
// section.
|
|
43
|
-
// `channel` is absent, fall back to the legacy `channelsConfig[mainChannel]`
|
|
44
|
-
// entry (or the first entry that carries an id).
|
|
42
|
+
// section. Disk config carries a single `channel` object only.
|
|
45
43
|
function resolveChannelId(raw = {}, backend = "discord") {
|
|
46
44
|
const channel = raw.channel && typeof raw.channel === "object" ? raw.channel : null;
|
|
47
45
|
if (channel) return channelIdForBackend(channel, backend);
|
|
48
|
-
const legacy = raw.channelsConfig && typeof raw.channelsConfig === "object" ? raw.channelsConfig : null;
|
|
49
|
-
if (legacy) {
|
|
50
|
-
const mainName = raw.mainChannel ?? "main";
|
|
51
|
-
const preferred = legacy[mainName];
|
|
52
|
-
if (preferred && typeof preferred === "object") {
|
|
53
|
-
const id = channelIdForBackend(preferred, backend);
|
|
54
|
-
if (id) return id;
|
|
55
|
-
}
|
|
56
|
-
for (const entry of Object.values(legacy)) {
|
|
57
|
-
if (entry && typeof entry === "object") {
|
|
58
|
-
const id = channelIdForBackend(entry, backend);
|
|
59
|
-
if (id) return id;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
46
|
return "";
|
|
64
47
|
}
|
|
65
48
|
|
|
@@ -96,13 +79,11 @@ async function loadConfig() {
|
|
|
96
79
|
const backend = raw.backend === "telegram" ? "telegram" : "discord";
|
|
97
80
|
const telegramToken = getTelegramToken();
|
|
98
81
|
const channelId = resolveChannelId(raw, backend);
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
// resolveChannelId — no on-disk migration).
|
|
102
|
-
const { channelsConfig: _legacyChannels, mainChannel: _legacyMain, ...rawRest } = raw;
|
|
82
|
+
// The runtime reads only the resolved `channelId`; disk carries a single
|
|
83
|
+
// `channel` object, so spread `raw` directly.
|
|
103
84
|
return applyDefaults({
|
|
104
85
|
...DEFAULT_CONFIG,
|
|
105
|
-
...
|
|
86
|
+
...raw,
|
|
106
87
|
backend,
|
|
107
88
|
channelId,
|
|
108
89
|
discord: { ...DEFAULT_CONFIG.discord, ...(({ token: _, ...rest }) => rest)(raw.discord || {}), ...(discordToken && !discordTokenProblem ? { token: discordToken } : {}) },
|
|
@@ -111,12 +92,7 @@ async function loadConfig() {
|
|
|
111
92
|
telegram: { ...DEFAULT_CONFIG.telegram, ...(({ token: _t, ...rest }) => rest)(raw.telegram || {}), ...(telegramToken ? { token: telegramToken } : {}) },
|
|
112
93
|
access: {
|
|
113
94
|
...DEFAULT_ACCESS,
|
|
114
|
-
|
|
115
|
-
// Discord backend's normalizeAccess() is the runtime belt): legacy
|
|
116
|
-
// dmPolicy "pairing" → "allowlist", and the `pending` code store is
|
|
117
|
-
// gone entirely.
|
|
118
|
-
...(({ pending: _pending, ...rest }) => rest)(raw.access || {}),
|
|
119
|
-
...(raw.access?.dmPolicy === "pairing" ? { dmPolicy: "allowlist" } : {}),
|
|
95
|
+
...(raw.access || {}),
|
|
120
96
|
channels: accessChannels,
|
|
121
97
|
},
|
|
122
98
|
voice: { ...(raw.voice || {}), ...voice }
|
|
@@ -315,14 +315,9 @@ export function diagnoseDiscordTokenValue(value, config = {}) {
|
|
|
315
315
|
const discord = config?.discord && typeof config.discord === 'object' ? config.discord : {}
|
|
316
316
|
const appId = String(discord.applicationId || '').trim()
|
|
317
317
|
if (appId && token === appId) return 'Bot token field contains the Application ID, not the bot token.'
|
|
318
|
-
// Single main channel: check the `channel` object.
|
|
319
|
-
// is still read here (read-only) so the diagnostic keeps working on configs
|
|
320
|
-
// that predate the single-channel collapse.
|
|
318
|
+
// Single main channel: check the `channel` object.
|
|
321
319
|
const candidateEntries = []
|
|
322
320
|
if (config?.channel && typeof config.channel === 'object') candidateEntries.push(config.channel)
|
|
323
|
-
if (config?.channelsConfig && typeof config.channelsConfig === 'object') {
|
|
324
|
-
candidateEntries.push(...Object.values(config.channelsConfig))
|
|
325
|
-
}
|
|
326
321
|
for (const ch of candidateEntries) {
|
|
327
322
|
if (!ch || typeof ch !== 'object') continue
|
|
328
323
|
for (const id of [ch.channelId, ch.discordChannelId, ch.telegramChatId]) {
|
|
@@ -3,10 +3,8 @@
|
|
|
3
3
|
* schedules (schema `scheduler`, table `scheduler.schedules`).
|
|
4
4
|
*
|
|
5
5
|
* All schedule readers/writers (scheduler.mjs, config.mjs, channel-admin.mjs)
|
|
6
|
-
* go through this module.
|
|
7
|
-
*
|
|
8
|
-
* renamed to `schedules.migrated`; the old file-based schedules-store.mjs has
|
|
9
|
-
* been removed.
|
|
6
|
+
* go through this module. It is the sole store for schedules; the old
|
|
7
|
+
* file-based schedules-store.mjs has been retired.
|
|
10
8
|
*
|
|
11
9
|
* DDL is idempotent and runs once on the first call per process. All queries
|
|
12
10
|
* fully-qualify `scheduler.schedules` so they are correct regardless of the
|
|
@@ -15,9 +13,6 @@
|
|
|
15
13
|
|
|
16
14
|
import { ensurePgInstance, withSchemaBootstrapLock } from '../memory/lib/pg/adapter.mjs';
|
|
17
15
|
import { resolvePluginData } from './plugin-paths.mjs';
|
|
18
|
-
import { readdirSync, readFileSync, renameSync, existsSync } from 'node:fs';
|
|
19
|
-
import { join } from 'node:path';
|
|
20
|
-
import { readMarkdownDocument } from './markdown-frontmatter.mjs';
|
|
21
16
|
|
|
22
17
|
const SCHEMA = 'scheduler';
|
|
23
18
|
|
|
@@ -58,7 +53,6 @@ async function getDb(dataDir = resolvePluginData()) {
|
|
|
58
53
|
// same cluster-global advisory lock the adapter uses for schema bootstrap,
|
|
59
54
|
// so racing first calls can't run the DDL simultaneously.
|
|
60
55
|
await withSchemaBootstrapLock(pool, () => db.exec(DDL));
|
|
61
|
-
await migrateLegacySchedules(db, dataDir);
|
|
62
56
|
return db;
|
|
63
57
|
})();
|
|
64
58
|
_ready.set(dataDir, p);
|
|
@@ -70,118 +64,6 @@ async function getDb(dataDir = resolvePluginData()) {
|
|
|
70
64
|
}
|
|
71
65
|
}
|
|
72
66
|
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
// One-time legacy SCHEDULE.md migration (additive; runs once per dataDir on
|
|
75
|
-
// first getDb, right after DDL). Imports every `<dataDir>/schedules/<name>/
|
|
76
|
-
// SCHEDULE.md` not already present in the table (by name), using the same
|
|
77
|
-
// days->cron folding as the admin write path, then renames the directory to
|
|
78
|
-
// `schedules.migrated` (never deletes user data). Per-entry failures are
|
|
79
|
-
// isolated and never block store readiness.
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
|
|
82
|
-
// Day-name / keyword -> cron day-of-week number (Sun=0 .. Sat=6).
|
|
83
|
-
const MIGRATE_DAY_TO_DOW = {
|
|
84
|
-
sun: 0, sunday: 0,
|
|
85
|
-
mon: 1, monday: 1,
|
|
86
|
-
tue: 2, tues: 2, tuesday: 2,
|
|
87
|
-
wed: 3, weds: 3, wednesday: 3,
|
|
88
|
-
thu: 4, thur: 4, thurs: 4, thursday: 4,
|
|
89
|
-
fri: 5, friday: 5,
|
|
90
|
-
sat: 6, saturday: 6,
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
function foldLegacyDaysIntoCron(cron, days) {
|
|
94
|
-
const parts = String(cron || '').trim().split(/\s+/).filter(Boolean);
|
|
95
|
-
if (parts.length !== 5 && parts.length !== 6) {
|
|
96
|
-
throw new Error(`invalid cron "${cron}"`);
|
|
97
|
-
}
|
|
98
|
-
const raw = String(days || '').trim().toLowerCase();
|
|
99
|
-
const dowIndex = parts.length - 1;
|
|
100
|
-
// days absent -> keep the cron's own day-of-week field ('0 9 * * 1' stays
|
|
101
|
-
// Monday-only). Only an explicit selector rewrites the dow field.
|
|
102
|
-
if (!raw) return parts.join(' ');
|
|
103
|
-
let dow;
|
|
104
|
-
if (raw === 'daily' || raw === 'everyday' || raw === 'every day') dow = '*';
|
|
105
|
-
else if (raw === 'weekday' || raw === 'weekdays') dow = '1-5';
|
|
106
|
-
else if (raw === 'weekend' || raw === 'weekends') dow = '0,6';
|
|
107
|
-
else {
|
|
108
|
-
const nums = raw.split(/[\s,]+/).filter(Boolean).map((t) => (
|
|
109
|
-
/^[0-6]$/.test(t) ? Number(t) : MIGRATE_DAY_TO_DOW[t]
|
|
110
|
-
));
|
|
111
|
-
if (nums.some((n) => n === undefined)) {
|
|
112
|
-
throw new Error(`days "${days}" is not a recognizable day selector`);
|
|
113
|
-
}
|
|
114
|
-
dow = nums.join(',');
|
|
115
|
-
}
|
|
116
|
-
parts[dowIndex] = dow;
|
|
117
|
-
return parts.join(' ');
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async function migrateLegacySchedules(db, dataDir) {
|
|
121
|
-
const dir = join(dataDir, 'schedules');
|
|
122
|
-
let entries;
|
|
123
|
-
try {
|
|
124
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
125
|
-
} catch {
|
|
126
|
-
return; // no legacy schedules/ dir -> nothing to migrate
|
|
127
|
-
}
|
|
128
|
-
let imported = 0;
|
|
129
|
-
let skipped = 0;
|
|
130
|
-
const failed = [];
|
|
131
|
-
for (const ent of entries) {
|
|
132
|
-
if (!ent.isDirectory()) continue;
|
|
133
|
-
const name = ent.name;
|
|
134
|
-
try {
|
|
135
|
-
const { rows } = await db.query('SELECT 1 FROM scheduler.schedules WHERE name = $1', [name]);
|
|
136
|
-
if (rows.length) { skipped++; continue; }
|
|
137
|
-
let md;
|
|
138
|
-
try { md = readFileSync(join(dir, name, 'SCHEDULE.md'), 'utf8'); }
|
|
139
|
-
catch { skipped++; continue; }
|
|
140
|
-
const { frontmatter, body } = readMarkdownDocument(md);
|
|
141
|
-
const cron = foldLegacyDaysIntoCron(frontmatter.time, frontmatter.days);
|
|
142
|
-
const channel = String(frontmatter.channel || '').trim();
|
|
143
|
-
const enabled = frontmatter.enabled !== 'false' && frontmatter.enabled !== false;
|
|
144
|
-
await db.query(
|
|
145
|
-
`INSERT INTO scheduler.schedules
|
|
146
|
-
(name, description, when_cron, timezone, target, channel_id, model, prompt, enabled)
|
|
147
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
|
|
148
|
-
ON CONFLICT (name) DO NOTHING`,
|
|
149
|
-
[
|
|
150
|
-
name,
|
|
151
|
-
String(frontmatter.description || '').trim(),
|
|
152
|
-
cron,
|
|
153
|
-
frontmatter.timezone ? String(frontmatter.timezone).trim() : null,
|
|
154
|
-
channel ? 'channel' : 'session',
|
|
155
|
-
channel || null,
|
|
156
|
-
frontmatter.model ? String(frontmatter.model).trim() : null,
|
|
157
|
-
String(body || '').trim(),
|
|
158
|
-
enabled,
|
|
159
|
-
],
|
|
160
|
-
);
|
|
161
|
-
imported++;
|
|
162
|
-
} catch (err) {
|
|
163
|
-
failed.push(name);
|
|
164
|
-
console.error(`[schedules] migration failed for "${name}": ${err?.message || err}`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
if (failed.length) {
|
|
168
|
-
// Some entries failed to import. Leave schedules/ in place (do NOT rename)
|
|
169
|
-
// so the next boot retries them; already-imported entries are skipped by
|
|
170
|
-
// the name check above, so retry is idempotent.
|
|
171
|
-
console.error(`[schedules] migrated ${imported} legacy schedule(s), ${failed.length} failed (${failed.join(', ')}); leaving schedules/ for retry`);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
try {
|
|
175
|
-
let target = `${dir}.migrated`;
|
|
176
|
-
if (existsSync(target)) target = `${dir}.migrated-${Date.now()}`;
|
|
177
|
-
renameSync(dir, target);
|
|
178
|
-
} catch (err) {
|
|
179
|
-
console.error(`[schedules] migrated ${imported} legacy schedule(s) but could not rename schedules/: ${err?.message || err}`);
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
console.error(`[schedules] migrated ${imported} legacy schedule(s) (${skipped} skipped); renamed schedules/ -> schedules.migrated`);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
67
|
// ---------------------------------------------------------------------------
|
|
186
68
|
// Row <-> def mapping
|
|
187
69
|
// ---------------------------------------------------------------------------
|
|
@@ -6,11 +6,9 @@
|
|
|
6
6
|
* This module mirrors schedules-db.mjs: a lazy connection keyed per resolved
|
|
7
7
|
* dataDir runs idempotent DDL exactly once per process, serialized across
|
|
8
8
|
* concurrent first-boot processes on the adapter's schema-bootstrap advisory
|
|
9
|
-
* lock. It
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* per-endpoint WEBHOOK.md + deliveries.jsonl files later. No call-site
|
|
13
|
-
* changes are made in this scope.
|
|
9
|
+
* lock. It is the sole store for endpoints + delivery dedup; the old
|
|
10
|
+
* per-endpoint WEBHOOK.md + deliveries.jsonl file store is fully retired
|
|
11
|
+
* (webhook.mjs reads/writes only through here).
|
|
14
12
|
*
|
|
15
13
|
* All queries fully-qualify their tables so they are correct regardless of
|
|
16
14
|
* the connection search_path.
|
|
@@ -18,9 +16,6 @@
|
|
|
18
16
|
|
|
19
17
|
import { ensurePgInstance, withSchemaBootstrapLock } from '../memory/lib/pg/adapter.mjs';
|
|
20
18
|
import { resolvePluginData } from './plugin-paths.mjs';
|
|
21
|
-
import { readdirSync, readFileSync, rmSync, rmdirSync } from 'node:fs';
|
|
22
|
-
import { join } from 'node:path';
|
|
23
|
-
import { readMarkdownDocument } from './markdown-frontmatter.mjs';
|
|
24
19
|
|
|
25
20
|
const SCHEMA = 'webhooks';
|
|
26
21
|
|
|
@@ -67,7 +62,6 @@ async function getDb(dataDir = resolvePluginData()) {
|
|
|
67
62
|
// same cluster-global advisory lock the adapter uses for schema bootstrap,
|
|
68
63
|
// so racing first calls can't run the DDL simultaneously.
|
|
69
64
|
await withSchemaBootstrapLock(pool, () => db.exec(DDL));
|
|
70
|
-
await migrateLegacyWebhooks(db, dataDir);
|
|
71
65
|
return db;
|
|
72
66
|
})();
|
|
73
67
|
_ready.set(dataDir, p);
|
|
@@ -79,91 +73,6 @@ async function getDb(dataDir = resolvePluginData()) {
|
|
|
79
73
|
}
|
|
80
74
|
}
|
|
81
75
|
|
|
82
|
-
// ---------------------------------------------------------------------------
|
|
83
|
-
// One-time legacy WEBHOOK.md migration (additive; runs once per dataDir on
|
|
84
|
-
// first getDb, right after DDL). Imports every `<dataDir>/webhooks/<name>/
|
|
85
|
-
// WEBHOOK.md` (+ its `secret` side file) not already present in the table (by
|
|
86
|
-
// name), then DELETES the webhooks/ directory. The legacy file reading is
|
|
87
|
-
// inlined here on purpose so this migration never depends on the file-store
|
|
88
|
-
// deliveries.mjs module (which another scope may remove). Old per-endpoint
|
|
89
|
-
// deliveries files are intentionally NOT imported — dedup history resets,
|
|
90
|
-
// which is acceptable. Partial failure keeps the directory in place and logs,
|
|
91
|
-
// so the next boot retries the un-imported entries idempotently.
|
|
92
|
-
// ---------------------------------------------------------------------------
|
|
93
|
-
async function migrateLegacyWebhooks(db, dataDir) {
|
|
94
|
-
const dir = join(dataDir, 'webhooks');
|
|
95
|
-
let entries;
|
|
96
|
-
try {
|
|
97
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
98
|
-
} catch {
|
|
99
|
-
return; // no legacy webhooks/ dir -> nothing to migrate
|
|
100
|
-
}
|
|
101
|
-
let imported = 0;
|
|
102
|
-
let skipped = 0;
|
|
103
|
-
const failed = [];
|
|
104
|
-
// Per-endpoint dirs safe to delete this run: those imported now, or already
|
|
105
|
-
// present in the table (a prior run imported them). Anything unrecognized —
|
|
106
|
-
// a dir with no WEBHOOK.md, a stray file, a failed import — is left in place.
|
|
107
|
-
const removable = [];
|
|
108
|
-
for (const ent of entries) {
|
|
109
|
-
if (!ent.isDirectory()) continue;
|
|
110
|
-
const name = ent.name;
|
|
111
|
-
try {
|
|
112
|
-
const { rows } = await db.query('SELECT 1 FROM webhooks.endpoints WHERE name = $1', [name]);
|
|
113
|
-
if (rows.length) { skipped++; removable.push(name); continue; }
|
|
114
|
-
let md;
|
|
115
|
-
try { md = readFileSync(join(dir, name, 'WEBHOOK.md'), 'utf8'); }
|
|
116
|
-
catch { skipped++; continue; }
|
|
117
|
-
const { frontmatter, body } = readMarkdownDocument(md);
|
|
118
|
-
let secret = null;
|
|
119
|
-
try { secret = String(readFileSync(join(dir, name, 'secret'), 'utf8')).trim() || null; }
|
|
120
|
-
catch { secret = null; }
|
|
121
|
-
const channel = String(frontmatter.channel || '').trim();
|
|
122
|
-
const enabled = frontmatter.enabled !== 'false' && frontmatter.enabled !== false;
|
|
123
|
-
await db.query(
|
|
124
|
-
`INSERT INTO webhooks.endpoints
|
|
125
|
-
(name, description, channel_id, role, model, parser, secret, instructions, enabled)
|
|
126
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
|
|
127
|
-
ON CONFLICT (name) DO NOTHING`,
|
|
128
|
-
[
|
|
129
|
-
name,
|
|
130
|
-
String(frontmatter.description || '').trim(),
|
|
131
|
-
channel || null,
|
|
132
|
-
'webhook-handler',
|
|
133
|
-
frontmatter.model ? String(frontmatter.model).trim() : null,
|
|
134
|
-
frontmatter.parser ? String(frontmatter.parser).trim() : null,
|
|
135
|
-
secret,
|
|
136
|
-
String(body || '').trim(),
|
|
137
|
-
enabled,
|
|
138
|
-
],
|
|
139
|
-
);
|
|
140
|
-
imported++;
|
|
141
|
-
removable.push(name);
|
|
142
|
-
} catch (err) {
|
|
143
|
-
failed.push(name);
|
|
144
|
-
console.error(`[webhooks] migration failed for "${name}": ${err?.message || err}`);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
// User chose deletion over a `.migrated` rename. Delete only the recognized
|
|
148
|
-
// per-endpoint dirs (imported now or already in the table); never blow away
|
|
149
|
-
// unimported/unrelated entries. Failed imports keep their dir for the next
|
|
150
|
-
// boot to retry (idempotent via the name check above).
|
|
151
|
-
for (const name of removable) {
|
|
152
|
-
try { rmSync(join(dir, name), { recursive: true, force: true }); }
|
|
153
|
-
catch (err) { console.error(`[webhooks] could not delete migrated dir "${name}": ${err?.message || err}`); }
|
|
154
|
-
}
|
|
155
|
-
// Remove the parent webhooks/ only once it is empty — anything unrecognized
|
|
156
|
-
// (stray files, WEBHOOK.md-less dirs, failed imports) keeps it alive.
|
|
157
|
-
let parentGone = false;
|
|
158
|
-
try {
|
|
159
|
-
if (readdirSync(dir).length === 0) { rmdirSync(dir); parentGone = true; }
|
|
160
|
-
} catch (err) {
|
|
161
|
-
console.error(`[webhooks] could not remove empty webhooks/: ${err?.message || err}`);
|
|
162
|
-
}
|
|
163
|
-
const tail = failed.length ? `${failed.length} failed (${failed.join(', ')}); ` : '';
|
|
164
|
-
console.error(`[webhooks] migrated ${imported} legacy webhook(s) (${skipped} skipped); ${tail}${parentGone ? 'removed webhooks/' : 'kept webhooks/ (non-empty)'}`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
76
|
// ---------------------------------------------------------------------------
|
|
168
77
|
// Row <-> def mapping
|
|
169
78
|
// ---------------------------------------------------------------------------
|
|
@@ -340,30 +249,6 @@ export async function claimDelivery(endpoint, deliveryId, fields = {}, { dataDir
|
|
|
340
249
|
return { claimed: false, duplicate: true, row: rowToDelivery(existing[0]) };
|
|
341
250
|
}
|
|
342
251
|
|
|
343
|
-
/**
|
|
344
|
-
* True when a delivery id has already been recorded for the endpoint (any
|
|
345
|
-
* status). Callers wanting terminal-vs-inflight semantics inspect the row via
|
|
346
|
-
* getDelivery; claimDelivery is the race-safe gate.
|
|
347
|
-
*/
|
|
348
|
-
export async function deliveryExists(endpoint, deliveryId, { dataDir } = {}) {
|
|
349
|
-
if (!endpoint || !deliveryId) return false;
|
|
350
|
-
const db = await getDb(dataDir);
|
|
351
|
-
const { rows } = await db.query(
|
|
352
|
-
'SELECT 1 FROM webhooks.deliveries WHERE endpoint = $1 AND delivery_id = $2',
|
|
353
|
-
[endpoint, deliveryId],
|
|
354
|
-
);
|
|
355
|
-
return rows.length > 0;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
export async function getDelivery(endpoint, deliveryId, { dataDir } = {}) {
|
|
359
|
-
const db = await getDb(dataDir);
|
|
360
|
-
const { rows } = await db.query(
|
|
361
|
-
`SELECT ${DELIVERY_COLS} FROM webhooks.deliveries WHERE endpoint = $1 AND delivery_id = $2`,
|
|
362
|
-
[endpoint, deliveryId],
|
|
363
|
-
);
|
|
364
|
-
return rowToDelivery(rows[0]);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
252
|
/**
|
|
368
253
|
* Update the status (and optional event/error) of an existing delivery,
|
|
369
254
|
* keyed by (endpoint, delivery_id). Returns the updated row or null.
|
|
@@ -383,23 +268,3 @@ export async function updateDeliveryStatus(endpoint, deliveryId, status, fields
|
|
|
383
268
|
);
|
|
384
269
|
return rowToDelivery(rows[0]);
|
|
385
270
|
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Append a delivery record (claim-or-update convenience covering the old
|
|
389
|
-
* appendDelivery). First write for an id claims it; a later write with the
|
|
390
|
-
* same id updates its status/fields latest-wins.
|
|
391
|
-
*
|
|
392
|
-
* Returns the claim outcome — { claimed, duplicate, row } — NOT a bare row,
|
|
393
|
-
* so a pre-dispatch caller can detect a concurrent duplicate (claimed:false,
|
|
394
|
-
* duplicate:true) and skip re-dispatching an in-flight delivery. `row` is the
|
|
395
|
-
* freshly claimed row when claimed, else the existing row after the
|
|
396
|
-
* latest-wins status/fields update.
|
|
397
|
-
*/
|
|
398
|
-
export async function appendDelivery(endpoint, entry = {}, { dataDir } = {}) {
|
|
399
|
-
const deliveryId = entry.deliveryId ?? entry.id;
|
|
400
|
-
if (!endpoint || !deliveryId) throw new Error('appendDelivery: endpoint and deliveryId are required');
|
|
401
|
-
const claim = await claimDelivery(endpoint, deliveryId, entry, { dataDir });
|
|
402
|
-
if (claim.claimed) return claim;
|
|
403
|
-
const row = await updateDeliveryStatus(endpoint, deliveryId, entry.status ?? claim.row?.status, entry, { dataDir });
|
|
404
|
-
return { claimed: false, duplicate: true, row };
|
|
405
|
-
}
|
|
@@ -102,42 +102,22 @@ function seedBackendChannelIds(entry = {}, backend = 'discord') {
|
|
|
102
102
|
return next;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
// Resolve the single-channel entry from the config
|
|
106
|
-
// object; fall back (read-side only, no on-disk migration) to the legacy
|
|
107
|
-
// channelsConfig[mainChannel ?? 'main'] entry, then the first entry with an id.
|
|
105
|
+
// Resolve the single-channel entry from the config's `channel` object.
|
|
108
106
|
function resolveChannelEntry(cfg = {}) {
|
|
109
107
|
if (cfg.channel && typeof cfg.channel === 'object'
|
|
110
108
|
&& (cfg.channel.channelId || cfg.channel.discordChannelId || cfg.channel.telegramChatId)) {
|
|
111
109
|
return cfg.channel;
|
|
112
110
|
}
|
|
113
|
-
const legacy = cfg.channelsConfig && typeof cfg.channelsConfig === 'object' ? cfg.channelsConfig : null;
|
|
114
|
-
if (legacy) {
|
|
115
|
-
const mainName = cfg.mainChannel ?? 'main';
|
|
116
|
-
const preferred = legacy[mainName];
|
|
117
|
-
if (preferred && typeof preferred === 'object'
|
|
118
|
-
&& (preferred.channelId || preferred.discordChannelId || preferred.telegramChatId)) {
|
|
119
|
-
return preferred;
|
|
120
|
-
}
|
|
121
|
-
for (const entry of Object.values(legacy)) {
|
|
122
|
-
if (entry && typeof entry === 'object'
|
|
123
|
-
&& (entry.channelId || entry.discordChannelId || entry.telegramChatId)) {
|
|
124
|
-
return entry;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
111
|
return cfg.channel && typeof cfg.channel === 'object' ? cfg.channel : {};
|
|
129
112
|
}
|
|
130
113
|
|
|
131
114
|
function updateChannelsSection(build) {
|
|
132
115
|
let next;
|
|
133
116
|
updateSection('channels', (current) => {
|
|
134
|
-
//
|
|
135
|
-
// for read-side compat, but never re-emit them from our writers: strip them
|
|
136
|
-
// out of the returned shape so writes converge on the single `channel`.
|
|
117
|
+
// Writes converge on the single `channel` object.
|
|
137
118
|
const normalized = normalizeChannelsConfig(current);
|
|
138
119
|
next = build(normalized);
|
|
139
|
-
|
|
140
|
-
return clean;
|
|
120
|
+
return normalizeChannelsConfig(next);
|
|
141
121
|
});
|
|
142
122
|
return next;
|
|
143
123
|
}
|
|
@@ -151,8 +131,7 @@ async function updateChannelsSectionAsync(build) {
|
|
|
151
131
|
await updateSectionAsync('channels', (current) => {
|
|
152
132
|
const normalized = normalizeChannelsConfig(current);
|
|
153
133
|
next = build(normalized);
|
|
154
|
-
|
|
155
|
-
return clean;
|
|
134
|
+
return normalizeChannelsConfig(next);
|
|
156
135
|
});
|
|
157
136
|
return next;
|
|
158
137
|
}
|
|
@@ -168,8 +147,8 @@ function listEntryDirs(dir) {
|
|
|
168
147
|
}
|
|
169
148
|
}
|
|
170
149
|
|
|
171
|
-
// Single-channel read: resolves `cfg.channel`
|
|
172
|
-
//
|
|
150
|
+
// Single-channel read: resolves `cfg.channel` into the flat shape the
|
|
151
|
+
// settings/TUI layer consumes.
|
|
173
152
|
export function getChannel(config = {}) {
|
|
174
153
|
const cfg = normalizeChannelsConfig(config);
|
|
175
154
|
const backend = cfg.backend === 'telegram' ? 'telegram' : 'discord';
|
|
@@ -38,9 +38,12 @@ const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme
|
|
|
38
38
|
* markers (agent card ←/→, history ↑/↓): WT draws them 1 cell in
|
|
39
39
|
* Cascadia, and widening them ate the marker's gutter padding space
|
|
40
40
|
* ("←Spawn" rendered glued / shifted vs the ● rows).
|
|
41
|
+
* Also excludes U+21BB (↻, the statusline quota-reset marker): WT/Cascadia
|
|
42
|
+
* draws it 1 cell, so widening it reserved a phantom cell that shifted the
|
|
43
|
+
* right-aligned workflow label one column left when the usage segment appeared.
|
|
41
44
|
*/
|
|
42
45
|
export function isProblemCodePoint(cp) {
|
|
43
|
-
return (cp >= 0x2460 && cp <= 0x24ff) || (cp >= 0x2194 && cp <= 0x21ff);
|
|
46
|
+
return (cp >= 0x2460 && cp <= 0x24ff) || (cp >= 0x2194 && cp <= 0x21ff && cp !== 0x21bb);
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
// Fast precheck for the problem ranges above. Lets the hot path bail before
|
package/src/tui/dist/index.mjs
CHANGED
|
@@ -4342,7 +4342,7 @@ import { Box as Box3, Text as Text3, useInput, usePaste, useStdin } from "../../
|
|
|
4342
4342
|
import stringWidth from "string-width";
|
|
4343
4343
|
var graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
4344
4344
|
function isProblemCodePoint(cp) {
|
|
4345
|
-
return cp >= 9312 && cp <= 9471 || cp >= 8596 && cp <= 8703;
|
|
4345
|
+
return cp >= 9312 && cp <= 9471 || cp >= 8596 && cp <= 8703 && cp !== 8635;
|
|
4346
4346
|
}
|
|
4347
4347
|
var PROBLEM_RE = /[\u2194-\u21ff\u2460-\u24ff]/;
|
|
4348
4348
|
function resolveAmbiguousWidePolicy(env = process.env, platform = process.platform) {
|
|
@@ -23,14 +23,14 @@ Lead supervises: delegates, coordinates, judges, decides. Route by complexity
|
|
|
23
23
|
1. Plan — present a draft plan before ANY implementation; if not approved,
|
|
24
24
|
revise and re-present (ping-pong) until an explicit go-ahead.
|
|
25
25
|
2. Delegate — maximize distribution: split the work into as many independent
|
|
26
|
-
scopes as possible and hand each to its own
|
|
27
|
-
turn (parallel by default; sequential
|
|
28
|
-
gated build/test-green).
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
implementation scopes as possible and hand each to its own worker or
|
|
27
|
+
heavy-worker, all spawned in the SAME turn (parallel by default; sequential
|
|
28
|
+
steps only inside one complex scope, gated build/test-green). Fan them out
|
|
29
|
+
across agents, never one at a time. Only a genuinely inseparable single
|
|
30
|
+
scope stays whole, and say so. Briefs per the Lead brief contract. After
|
|
31
|
+
spawning async agents, END THE TURN.
|
|
32
|
+
3. Review — spawn one reviewer 1:1 per implementation scope, all reviewers in
|
|
33
|
+
the same turn once their scopes land.
|
|
34
34
|
Cross-check agent results yourself; send fixes back to the original scope
|
|
35
35
|
and loop fix -> re-verify until clean. Skip only for simple low-risk work.
|
|
36
36
|
Debugger first when the user asks for debugging or a bug survives 2+ fix
|
|
@@ -39,7 +39,7 @@ Lead supervises: delegates, coordinates, judges, decides. Route by complexity
|
|
|
39
39
|
and how work proceeds, marked in-progress — never as a conclusion.
|
|
40
40
|
4. Report — final report briefs the whole work vs the approved plan and the
|
|
41
41
|
verified result, distinct from interim updates. Never forward raw agent
|
|
42
|
-
output. Ask about ship/deploy when relevant; deploy/build/commit only
|
|
43
|
-
|
|
42
|
+
output. Ask about ship/deploy when relevant; deploy/build/commit only on an
|
|
43
|
+
explicit user request, after feedback with no issues.
|
|
44
44
|
|
|
45
45
|
On major direction shifts mid-work, pause and re-consult the user.
|
|
@@ -24,7 +24,9 @@ import stringWidth from 'string-width';
|
|
|
24
24
|
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
25
25
|
|
|
26
26
|
function isProblemCodePoint(cp) {
|
|
27
|
-
|
|
27
|
+
// [mixdog fork] U+21BB (↻ quota-reset marker) excluded: WT draws it 1 cell,
|
|
28
|
+
// so widening it shifted the right-aligned statusline label one col left.
|
|
29
|
+
return (cp >= 0x2460 && cp <= 0x24ff) || (cp >= 0x2194 && cp <= 0x21ff && cp !== 0x21bb);
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
// [mixdog fork] Fast precheck for the problem ranges above. Lets the hot path
|