u-foo 2.3.10 → 2.3.12
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 +3 -1
- package/src/agent/activityStateWriter.js +14 -3
- package/src/agent/claudeThreadProvider.js +208 -39
- package/src/agent/codexThreadProvider.js +125 -64
- package/src/agent/internalRunner.js +191 -22
- package/src/agent/notifier.js +15 -4
- package/src/agent/ptyRunner.js +8 -92
- package/src/bus/index.js +2 -1
- package/src/bus/store.js +17 -5
- package/src/bus/subscriber.js +57 -1
- package/src/bus/utils.js +6 -0
- package/src/chat/agentViewController.js +4 -1
- package/src/chat/dashboardKeyController.js +17 -2
- package/src/ufoo/agentRegistryDiagnostics.js +91 -0
- package/src/ufoo/agentsStore.js +38 -2
package/src/bus/store.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require("path");
|
|
|
5
5
|
const { getTimestamp, ensureDir, safeNameToSubscriber, getTtyProcessInfo } = require("./utils");
|
|
6
6
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
7
7
|
const { loadAgentsData, saveAgentsData } = require("../ufoo/agentsStore");
|
|
8
|
+
const { appendAgentRegistryDiagnostic } = require("../ufoo/agentRegistryDiagnostics");
|
|
8
9
|
|
|
9
10
|
function readQueueTty(queueDir) {
|
|
10
11
|
try {
|
|
@@ -25,7 +26,7 @@ function buildUsedNicknameSet(agents = {}) {
|
|
|
25
26
|
return set;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
function recoverQueueEntry(data, subscriber, queueDir, usedNicknames, now) {
|
|
29
|
+
function recoverQueueEntry(data, subscriber, queueDir, usedNicknames, now, agentsFile) {
|
|
29
30
|
if (!subscriber || data.agents[subscriber]) return false;
|
|
30
31
|
|
|
31
32
|
if (subscriber === "ufoo-agent") {
|
|
@@ -45,6 +46,17 @@ function recoverQueueEntry(data, subscriber, queueDir, usedNicknames, now) {
|
|
|
45
46
|
};
|
|
46
47
|
return true;
|
|
47
48
|
}
|
|
49
|
+
appendAgentRegistryDiagnostic(
|
|
50
|
+
agentsFile,
|
|
51
|
+
"queue_entry_not_recovered",
|
|
52
|
+
{
|
|
53
|
+
source: "bus.store.recoverQueueEntry",
|
|
54
|
+
subscriber,
|
|
55
|
+
queue_dir: queueDir,
|
|
56
|
+
reason: "non_controller_queue_without_registry_entry",
|
|
57
|
+
used_nicknames: Array.from(usedNicknames || []).sort(),
|
|
58
|
+
}
|
|
59
|
+
);
|
|
48
60
|
return false;
|
|
49
61
|
}
|
|
50
62
|
|
|
@@ -112,20 +124,20 @@ class BusStore {
|
|
|
112
124
|
if (!stat.isDirectory()) continue;
|
|
113
125
|
|
|
114
126
|
const subscriber = safeNameToSubscriber(entry);
|
|
115
|
-
recovered = recoverQueueEntry(data, subscriber, queueDir, usedNicknames, now) || recovered;
|
|
127
|
+
recovered = recoverQueueEntry(data, subscriber, queueDir, usedNicknames, now, this.agentsFile) || recovered;
|
|
116
128
|
}
|
|
117
129
|
|
|
118
130
|
recovered = reconcileReservedControllerAliases(data, now) || recovered;
|
|
119
131
|
|
|
120
132
|
if (recovered) {
|
|
121
|
-
saveAgentsData(this.agentsFile, data);
|
|
133
|
+
saveAgentsData(this.agentsFile, data, { source: "bus.store.load.recoverQueueEntry", trace: true });
|
|
122
134
|
}
|
|
123
135
|
return data;
|
|
124
136
|
}
|
|
125
137
|
|
|
126
138
|
save(busData) {
|
|
127
139
|
if (busData) {
|
|
128
|
-
saveAgentsData(this.agentsFile, busData);
|
|
140
|
+
saveAgentsData(this.agentsFile, busData, { source: "bus.store.save" });
|
|
129
141
|
}
|
|
130
142
|
}
|
|
131
143
|
|
|
@@ -144,7 +156,7 @@ class BusStore {
|
|
|
144
156
|
created_at: getTimestamp(),
|
|
145
157
|
agents: {},
|
|
146
158
|
};
|
|
147
|
-
saveAgentsData(this.agentsFile, busData);
|
|
159
|
+
saveAgentsData(this.agentsFile, busData, { source: "bus.store.init", trace: true });
|
|
148
160
|
}
|
|
149
161
|
}
|
|
150
162
|
|
package/src/bus/subscriber.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require("fs");
|
|
|
2
2
|
const { getTimestamp, isAgentPidAlive, isMetaActive, isValidTty, getTtyProcessInfo } = require("./utils");
|
|
3
3
|
const NicknameManager = require("./nickname");
|
|
4
4
|
const { spawnSync } = require("child_process");
|
|
5
|
+
const { appendAgentRegistryDiagnostic } = require("../ufoo/agentRegistryDiagnostics");
|
|
5
6
|
|
|
6
7
|
function detectTerminalAppFromEnv() {
|
|
7
8
|
const termProgram = process.env.TERM_PROGRAM || "";
|
|
@@ -102,9 +103,14 @@ function hasProviderSession(meta) {
|
|
|
102
103
|
* 订阅者管理
|
|
103
104
|
*/
|
|
104
105
|
class SubscriberManager {
|
|
105
|
-
constructor(busData, queueManager) {
|
|
106
|
+
constructor(busData, queueManager, options = {}) {
|
|
106
107
|
this.busData = busData;
|
|
107
108
|
this.queueManager = queueManager;
|
|
109
|
+
this.agentsFile = options.agentsFile || "";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
logRegistry(event, payload = {}) {
|
|
113
|
+
appendAgentRegistryDiagnostic(this.agentsFile, event, payload);
|
|
108
114
|
}
|
|
109
115
|
|
|
110
116
|
cleanupSubscriberArtifacts(subscriber) {
|
|
@@ -152,6 +158,15 @@ class SubscriberManager {
|
|
|
152
158
|
inheritedNickname = meta.nickname;
|
|
153
159
|
}
|
|
154
160
|
// Remove stale subscriber using same tty
|
|
161
|
+
this.logRegistry("cleanup_duplicate_tty", {
|
|
162
|
+
source: "bus.subscriber.cleanupDuplicateTty",
|
|
163
|
+
subscriber: id,
|
|
164
|
+
replacement: currentSubscriber,
|
|
165
|
+
tty: ttyPath,
|
|
166
|
+
same_agent_type: sameAgentType,
|
|
167
|
+
status: meta?.status || "",
|
|
168
|
+
nickname: meta?.nickname || "",
|
|
169
|
+
});
|
|
155
170
|
delete this.busData.agents[id];
|
|
156
171
|
try {
|
|
157
172
|
const queueDir = this.queueManager.getQueueDir(id);
|
|
@@ -420,6 +435,16 @@ class SubscriberManager {
|
|
|
420
435
|
const recoverable = hasProviderSession(meta);
|
|
421
436
|
if (meta.status === "inactive") {
|
|
422
437
|
if (!recoverable) {
|
|
438
|
+
this.logRegistry("cleanup_inactive_delete", {
|
|
439
|
+
source: "bus.subscriber.cleanupInactive",
|
|
440
|
+
subscriber: id,
|
|
441
|
+
reason: "internal_already_inactive_without_provider_session",
|
|
442
|
+
status: meta.status || "",
|
|
443
|
+
launch_mode: meta.launch_mode || "",
|
|
444
|
+
pid: meta.pid || 0,
|
|
445
|
+
tty: meta.tty || "",
|
|
446
|
+
last_seen: meta.last_seen || "",
|
|
447
|
+
});
|
|
423
448
|
delete this.busData.agents[id];
|
|
424
449
|
this.cleanupSubscriberArtifacts(id);
|
|
425
450
|
}
|
|
@@ -427,11 +452,31 @@ class SubscriberManager {
|
|
|
427
452
|
}
|
|
428
453
|
if (!isMetaActive(meta)) {
|
|
429
454
|
if (recoverable) {
|
|
455
|
+
this.logRegistry("cleanup_inactive_mark", {
|
|
456
|
+
source: "bus.subscriber.cleanupInactive",
|
|
457
|
+
subscriber: id,
|
|
458
|
+
reason: "internal_inactive_but_recoverable_provider_session",
|
|
459
|
+
status: meta.status || "",
|
|
460
|
+
launch_mode: meta.launch_mode || "",
|
|
461
|
+
pid: meta.pid || 0,
|
|
462
|
+
tty: meta.tty || "",
|
|
463
|
+
last_seen: meta.last_seen || "",
|
|
464
|
+
});
|
|
430
465
|
meta.status = "inactive";
|
|
431
466
|
meta.activity_state = "";
|
|
432
467
|
meta.last_seen = getTimestamp();
|
|
433
468
|
this.cleanupSubscriberArtifacts(id);
|
|
434
469
|
} else {
|
|
470
|
+
this.logRegistry("cleanup_inactive_delete", {
|
|
471
|
+
source: "bus.subscriber.cleanupInactive",
|
|
472
|
+
subscriber: id,
|
|
473
|
+
reason: "internal_inactive_without_provider_session",
|
|
474
|
+
status: meta.status || "",
|
|
475
|
+
launch_mode: meta.launch_mode || "",
|
|
476
|
+
pid: meta.pid || 0,
|
|
477
|
+
tty: meta.tty || "",
|
|
478
|
+
last_seen: meta.last_seen || "",
|
|
479
|
+
});
|
|
435
480
|
delete this.busData.agents[id];
|
|
436
481
|
this.cleanupSubscriberArtifacts(id);
|
|
437
482
|
}
|
|
@@ -439,6 +484,17 @@ class SubscriberManager {
|
|
|
439
484
|
continue;
|
|
440
485
|
}
|
|
441
486
|
if (meta.status === "active" && !isMetaActive(meta)) {
|
|
487
|
+
this.logRegistry("cleanup_inactive_mark", {
|
|
488
|
+
source: "bus.subscriber.cleanupInactive",
|
|
489
|
+
subscriber: id,
|
|
490
|
+
reason: "active_meta_failed_liveness",
|
|
491
|
+
status: meta.status || "",
|
|
492
|
+
launch_mode: meta.launch_mode || "",
|
|
493
|
+
pid: meta.pid || 0,
|
|
494
|
+
tty: meta.tty || "",
|
|
495
|
+
tty_shell_pid: meta.tty_shell_pid || 0,
|
|
496
|
+
last_seen: meta.last_seen || "",
|
|
497
|
+
});
|
|
442
498
|
meta.status = "inactive";
|
|
443
499
|
meta.activity_state = "";
|
|
444
500
|
meta.last_seen = getTimestamp();
|
package/src/bus/utils.js
CHANGED
|
@@ -3,6 +3,7 @@ const fs = require("fs");
|
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const { spawnSync } = require("child_process");
|
|
5
5
|
const { redactSecrets } = require("../providerapi/redactor");
|
|
6
|
+
const { appendAgentRegistryDiagnostic } = require("../ufoo/agentRegistryDiagnostics");
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* 获取当前 UTC 时间戳(ISO 8601 格式)
|
|
@@ -200,6 +201,11 @@ function readJSON(filePath, defaultValue = null) {
|
|
|
200
201
|
const content = fs.readFileSync(filePath, "utf8");
|
|
201
202
|
return JSON.parse(content);
|
|
202
203
|
} catch (err) {
|
|
204
|
+
appendAgentRegistryDiagnostic(filePath, "read_json_failed", {
|
|
205
|
+
source: "bus.utils.readJSON",
|
|
206
|
+
error: err && err.message ? err.message : String(err || "unknown"),
|
|
207
|
+
default_returned: defaultValue === null ? "null" : typeof defaultValue,
|
|
208
|
+
});
|
|
203
209
|
return defaultValue;
|
|
204
210
|
}
|
|
205
211
|
}
|
|
@@ -126,7 +126,10 @@ function createAgentViewController(options = {}) {
|
|
|
126
126
|
|
|
127
127
|
agentInputSuppressUntil = now() + 300;
|
|
128
128
|
agentViewUsesBus = Boolean(options.useBus);
|
|
129
|
-
if (
|
|
129
|
+
if (agentViewUsesBus) {
|
|
130
|
+
const label = getAgentLabel(agentId);
|
|
131
|
+
processStdout.write(`ufoo internal · ${label}\r\n\r\n> `);
|
|
132
|
+
} else {
|
|
130
133
|
const sockPath = getInjectSockPath(agentId);
|
|
131
134
|
connectAgentOutput(sockPath);
|
|
132
135
|
connectAgentInput(sockPath);
|
|
@@ -57,6 +57,11 @@ function createDashboardKeyController(options = {}) {
|
|
|
57
57
|
return Boolean(caps && caps.supportsSocketProtocol);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function supportsInternalQueue(agentId) {
|
|
61
|
+
const caps = getAgentCapabilities(agentId);
|
|
62
|
+
return Boolean(caps && caps.supportsInternalQueueLoop);
|
|
63
|
+
}
|
|
64
|
+
|
|
60
65
|
function withAgentInputFocus() {
|
|
61
66
|
state.focusMode = "input";
|
|
62
67
|
state.agentOutputSuppressed = false;
|
|
@@ -73,7 +78,7 @@ function createDashboardKeyController(options = {}) {
|
|
|
73
78
|
|
|
74
79
|
function switchAgentView(agentId) {
|
|
75
80
|
withAgentInputFocus();
|
|
76
|
-
enterAgentView(agentId);
|
|
81
|
+
enterAgentView(agentId, { useBus: supportsInternalQueue(agentId) && !supportsSocket(agentId) });
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
function exitAgentDashboardToInput() {
|
|
@@ -154,7 +159,7 @@ function createDashboardKeyController(options = {}) {
|
|
|
154
159
|
} else {
|
|
155
160
|
withAgentInputFocus();
|
|
156
161
|
state.selectedAgentIndex = nextIndex + 1;
|
|
157
|
-
enterAgentView(nextAgent);
|
|
162
|
+
enterAgentView(nextAgent, { useBus: supportsInternalQueue(nextAgent) && !supportsSocket(nextAgent) });
|
|
158
163
|
}
|
|
159
164
|
} else {
|
|
160
165
|
exitAgentView();
|
|
@@ -511,6 +516,16 @@ function createDashboardKeyController(options = {}) {
|
|
|
511
516
|
enterAgentView(agentId);
|
|
512
517
|
return true;
|
|
513
518
|
}
|
|
519
|
+
|
|
520
|
+
if (supportsInternalQueue(agentId)) {
|
|
521
|
+
clearTargetAgent();
|
|
522
|
+
state.focusMode = "input";
|
|
523
|
+
state.dashboardView = "agents";
|
|
524
|
+
state.selectedAgentIndex = -1;
|
|
525
|
+
setScreenGrabKeys(false);
|
|
526
|
+
enterAgentView(agentId, { useBus: true });
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
514
529
|
}
|
|
515
530
|
|
|
516
531
|
exitDashboardMode(false);
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
function isAgentsFile(filePath) {
|
|
5
|
+
return path.basename(filePath || "") === "all-agents.json"
|
|
6
|
+
&& path.basename(path.dirname(filePath || "")) === "agent";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function getRegistryLogPath(agentsFilePath) {
|
|
10
|
+
const ufooRoot = path.dirname(path.dirname(agentsFilePath));
|
|
11
|
+
return path.join(ufooRoot, "run", "agent-registry-diagnostics.log");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function summarizeFile(filePath) {
|
|
15
|
+
try {
|
|
16
|
+
const stat = fs.statSync(filePath);
|
|
17
|
+
return {
|
|
18
|
+
exists: true,
|
|
19
|
+
size: stat.size,
|
|
20
|
+
mtime: stat.mtime.toISOString(),
|
|
21
|
+
};
|
|
22
|
+
} catch (err) {
|
|
23
|
+
return {
|
|
24
|
+
exists: false,
|
|
25
|
+
error: err && err.code ? err.code : String(err || "unknown"),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function summarizeAgents(data) {
|
|
31
|
+
const agents = data && typeof data === "object" && data.agents && typeof data.agents === "object"
|
|
32
|
+
? data.agents
|
|
33
|
+
: {};
|
|
34
|
+
const ids = Object.keys(agents).sort();
|
|
35
|
+
const statuses = {};
|
|
36
|
+
const nicknames = {};
|
|
37
|
+
for (const id of ids) {
|
|
38
|
+
const meta = agents[id] || {};
|
|
39
|
+
const status = typeof meta.status === "string" && meta.status ? meta.status : "unknown";
|
|
40
|
+
statuses[status] = (statuses[status] || 0) + 1;
|
|
41
|
+
if (typeof meta.nickname === "string" && meta.nickname) {
|
|
42
|
+
nicknames[id] = meta.nickname;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
count: ids.length,
|
|
47
|
+
ids,
|
|
48
|
+
statuses,
|
|
49
|
+
nicknames,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function safePayload(payload = {}) {
|
|
54
|
+
const out = {};
|
|
55
|
+
for (const [key, value] of Object.entries(payload || {})) {
|
|
56
|
+
if (/token|secret|password|credential|auth/i.test(key)) {
|
|
57
|
+
out[key] = "[REDACTED]";
|
|
58
|
+
} else {
|
|
59
|
+
out[key] = value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function appendAgentRegistryDiagnostic(agentsFilePath, event, payload = {}) {
|
|
66
|
+
if (!agentsFilePath || !isAgentsFile(agentsFilePath)) return;
|
|
67
|
+
try {
|
|
68
|
+
const logPath = getRegistryLogPath(agentsFilePath);
|
|
69
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
70
|
+
const line = JSON.stringify({
|
|
71
|
+
ts: new Date().toISOString(),
|
|
72
|
+
pid: process.pid,
|
|
73
|
+
ppid: process.ppid,
|
|
74
|
+
event,
|
|
75
|
+
agents_file: agentsFilePath,
|
|
76
|
+
file: summarizeFile(agentsFilePath),
|
|
77
|
+
...safePayload(payload),
|
|
78
|
+
});
|
|
79
|
+
fs.appendFileSync(logPath, `${line}\n`, "utf8");
|
|
80
|
+
} catch {
|
|
81
|
+
// Diagnostics must never affect agent liveness paths.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
appendAgentRegistryDiagnostic,
|
|
87
|
+
summarizeAgents,
|
|
88
|
+
summarizeFile,
|
|
89
|
+
isAgentsFile,
|
|
90
|
+
getRegistryLogPath,
|
|
91
|
+
};
|
package/src/ufoo/agentsStore.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { getTimestamp, readJSON, writeJSON } = require("../bus/utils");
|
|
2
|
+
const { appendAgentRegistryDiagnostic, summarizeAgents } = require("./agentRegistryDiagnostics");
|
|
2
3
|
|
|
3
4
|
const AGENTS_SCHEMA_VERSION = 1;
|
|
4
5
|
|
|
@@ -89,9 +90,23 @@ function normalizeAgentsData(data) {
|
|
|
89
90
|
function loadAgentsData(filePath) {
|
|
90
91
|
const data = readJSON(filePath, null);
|
|
91
92
|
if (!data) {
|
|
93
|
+
appendAgentRegistryDiagnostic(filePath, "load_agents_empty", {
|
|
94
|
+
source: "ufoo.agentsStore.loadAgentsData",
|
|
95
|
+
reason: "missing_or_unreadable_registry",
|
|
96
|
+
});
|
|
92
97
|
return normalizeAgentsData({});
|
|
93
98
|
}
|
|
94
|
-
|
|
99
|
+
const normalized = normalizeAgentsData(data);
|
|
100
|
+
const beforeSummary = summarizeAgents(data);
|
|
101
|
+
const afterSummary = summarizeAgents(normalized);
|
|
102
|
+
if (JSON.stringify(beforeSummary) !== JSON.stringify(afterSummary)) {
|
|
103
|
+
appendAgentRegistryDiagnostic(filePath, "load_agents_normalized", {
|
|
104
|
+
source: "ufoo.agentsStore.loadAgentsData",
|
|
105
|
+
before: beforeSummary,
|
|
106
|
+
after: afterSummary,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return normalized;
|
|
95
110
|
}
|
|
96
111
|
|
|
97
112
|
function parseTimestampMs(value) {
|
|
@@ -125,13 +140,27 @@ function mergeExternalActivityFields(targetMeta, diskMeta) {
|
|
|
125
140
|
}
|
|
126
141
|
}
|
|
127
142
|
|
|
128
|
-
function saveAgentsData(filePath, data) {
|
|
143
|
+
function saveAgentsData(filePath, data, options = {}) {
|
|
144
|
+
const source = typeof options.source === "string" && options.source
|
|
145
|
+
? options.source
|
|
146
|
+
: "ufoo.agentsStore.saveAgentsData";
|
|
129
147
|
const normalized = normalizeAgentsData(data);
|
|
130
148
|
|
|
131
149
|
// Merge externally-managed fields from disk to avoid daemon in-memory writes
|
|
132
150
|
// overwriting fresher runner/notifier state updates.
|
|
133
151
|
const disk = readJSON(filePath, null);
|
|
134
152
|
if (disk && disk.agents && normalized.agents) {
|
|
153
|
+
const droppedIds = Object.keys(disk.agents)
|
|
154
|
+
.filter((id) => !Object.prototype.hasOwnProperty.call(normalized.agents, id))
|
|
155
|
+
.sort();
|
|
156
|
+
if (droppedIds.length > 0) {
|
|
157
|
+
appendAgentRegistryDiagnostic(filePath, "save_agents_dropping_disk_entries", {
|
|
158
|
+
source,
|
|
159
|
+
dropped_ids: droppedIds,
|
|
160
|
+
disk: summarizeAgents(disk),
|
|
161
|
+
next: summarizeAgents(normalized),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
135
164
|
for (const [id, diskMeta] of Object.entries(disk.agents)) {
|
|
136
165
|
if (!diskMeta || typeof diskMeta !== "object") continue;
|
|
137
166
|
const targetMeta = normalized.agents[id];
|
|
@@ -140,6 +169,13 @@ function saveAgentsData(filePath, data) {
|
|
|
140
169
|
}
|
|
141
170
|
}
|
|
142
171
|
|
|
172
|
+
const nextSummary = summarizeAgents(normalized);
|
|
173
|
+
if (nextSummary.count === 0 || options.trace === true) {
|
|
174
|
+
appendAgentRegistryDiagnostic(filePath, "save_agents_data", {
|
|
175
|
+
source,
|
|
176
|
+
next: nextSummary,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
143
179
|
writeJSON(filePath, normalized);
|
|
144
180
|
}
|
|
145
181
|
|