@inetafrica/open-claudia 2.15.0 → 3.0.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 +30 -4
- package/CHANGELOG.md +12 -0
- package/README.md +87 -66
- package/bin/agent.js +44 -18
- package/bin/cli.js +30 -28
- package/bin/dream.js +5 -11
- package/bin/loopback-client.js +29 -5
- package/bin/schedule.js +19 -5
- package/bin/tool.js +23 -5
- package/bot-agent.js +5 -2350
- package/bot.js +66 -3
- package/channels/telegram/adapter.js +6 -1
- package/channels/voice/adapter.js +1 -0
- package/channels/voice/manage.js +43 -5
- package/config-dir.js +3 -11
- package/core/actions.js +149 -40
- package/core/approvals.js +136 -29
- package/core/auth-flow.js +103 -19
- package/core/config-dir.js +19 -0
- package/core/config.js +115 -69
- package/core/doctor.js +111 -57
- package/core/dream.js +219 -62
- package/core/enforcer.js +0 -0
- package/core/entities.js +2 -1
- package/core/fsutil.js +21 -4
- package/core/handlers.js +503 -217
- package/core/ideas.js +2 -1
- package/core/jobs.js +589 -72
- package/core/lessons.js +3 -2
- package/core/loopback.js +42 -6
- package/core/memory-mutation-queue.js +241 -0
- package/core/migration-backup.js +1060 -0
- package/core/pack-review.js +295 -88
- package/core/packs.js +4 -3
- package/core/persona.js +2 -1
- package/core/process-tree.js +19 -2
- package/core/provider-migration.js +39 -0
- package/core/provider-status.js +80 -0
- package/core/providers/claude-events.js +117 -0
- package/core/providers/claude-hook.js +114 -0
- package/core/providers/claude.js +296 -0
- package/core/providers/codex-events.js +121 -0
- package/core/providers/codex-hook.js +280 -0
- package/core/providers/codex.js +286 -0
- package/core/providers/contract.js +153 -0
- package/core/providers/env.js +148 -0
- package/core/providers/events.js +170 -0
- package/core/providers/hook-command.js +22 -0
- package/core/providers/index.js +240 -0
- package/core/providers/utility-policy.js +269 -0
- package/core/recall/classic.js +2 -2
- package/core/recall/discoverer.js +71 -22
- package/core/recall/metrics.js +27 -1
- package/core/recall/tuning.js +4 -1
- package/core/recall/warm-walker.js +151 -108
- package/core/recall-filter.js +86 -61
- package/core/router.js +79 -7
- package/core/run-context.js +185 -0
- package/core/runner.js +1414 -1290
- package/core/scheduler.js +515 -210
- package/core/side-chat.js +346 -0
- package/core/state.js +1084 -97
- package/core/subagent.js +72 -98
- package/core/system-prompt.js +462 -279
- package/core/tool-guard.js +73 -39
- package/core/tools.js +22 -5
- package/core/transcripts.js +30 -1
- package/core/usage-log.js +19 -4
- package/core/utility-agent.js +654 -0
- package/core/web-auth.js +29 -13
- package/docs/CHANNEL_DESIGN.md +181 -0
- package/docs/MULTI_CHANNEL_PLAN.md +254 -0
- package/docs/PROVIDER_MIGRATION.md +67 -0
- package/health.js +50 -39
- package/package.json +48 -4
- package/setup.js +198 -38
- package/soul.md +39 -0
- package/test-ability-extraction.js +5 -0
- package/test-approval-async.js +63 -4
- package/test-cursor-removal.js +334 -0
- package/test-enforcer.js +70 -27
- package/test-fixtures/current-provider-parity.json +132 -0
- package/test-fixtures/fake-agent-cli.js +477 -0
- package/test-fixtures/migrations/claude-only/jobs.json +3 -0
- package/test-fixtures/migrations/claude-only/sessions.json +5 -0
- package/test-fixtures/migrations/claude-only/state.json +5 -0
- package/test-fixtures/migrations/cursor-selected/crons.json +3 -0
- package/test-fixtures/migrations/cursor-selected/jobs.json +3 -0
- package/test-fixtures/migrations/cursor-selected/sessions.json +5 -0
- package/test-fixtures/migrations/cursor-selected/state.json +6 -0
- package/test-fixtures/migrations/dual-provider/jobs.json +3 -0
- package/test-fixtures/migrations/dual-provider/sessions.json +7 -0
- package/test-fixtures/migrations/dual-provider/state.json +10 -0
- package/test-fixtures/migrations/malformed-partial/jobs.json +1 -0
- package/test-fixtures/migrations/malformed-partial/sessions.json +1 -0
- package/test-fixtures/migrations/malformed-partial/sessions.json.bak +3 -0
- package/test-fixtures/migrations/malformed-partial/state.json +1 -0
- package/test-fixtures/migrations/malformed-partial/state.json.bak +5 -0
- package/test-fixtures/migrations/missing-project/jobs.json +3 -0
- package/test-fixtures/migrations/missing-project/sessions.json +3 -0
- package/test-fixtures/migrations/missing-project/state.json +4 -0
- package/test-fixtures/migrations/multi-user/jobs.json +4 -0
- package/test-fixtures/migrations/multi-user/sessions.json +4 -0
- package/test-fixtures/migrations/multi-user/state.json +6 -0
- package/test-fixtures/provider-event-samples.json +17 -0
- package/test-learning-e2e.js +5 -0
- package/test-memory-mutation-queue.js +191 -0
- package/test-provider-boot-matrix.js +399 -0
- package/test-provider-bootstrap.js +158 -0
- package/test-provider-capabilities.js +232 -0
- package/test-provider-claude.js +206 -0
- package/test-provider-codex.js +228 -0
- package/test-provider-compaction.js +371 -0
- package/test-provider-core-prompt.js +264 -0
- package/test-provider-coupling-audit.js +90 -0
- package/test-provider-dream.js +312 -0
- package/test-provider-enforcer.js +252 -0
- package/test-provider-env-isolation.js +150 -0
- package/test-provider-events.js +141 -0
- package/test-provider-fixture.js +332 -0
- package/test-provider-gateway.js +508 -0
- package/test-provider-language.js +89 -0
- package/test-provider-migration-backup.js +424 -0
- package/test-provider-pack-review.js +436 -0
- package/test-provider-parity-e2e.js +537 -0
- package/test-provider-prompt-parity.js +89 -0
- package/test-provider-recall.js +271 -0
- package/test-provider-registry.js +251 -0
- package/test-provider-resume-parity.js +41 -0
- package/test-provider-run-lifecycle.js +349 -0
- package/test-provider-runner.js +271 -0
- package/test-provider-scheduler.js +689 -0
- package/test-provider-session-commands.js +185 -0
- package/test-provider-session-history.js +205 -0
- package/test-provider-side-chat.js +337 -0
- package/test-provider-state-migration.js +316 -0
- package/test-provider-stream-decoder.js +69 -0
- package/test-provider-subagent.js +228 -0
- package/test-provider-tool-hooks.js +360 -0
- package/test-recall-discoverer.js +1 -0
- package/test-recall-engine.js +3 -3
- package/test-recall-evolution.js +18 -0
- package/test-runner-watchdog-static.js +16 -8
- package/test-single-runtime.js +35 -0
- package/test-telegram-poll-recovery.js +56 -0
- package/test-tool-guard.js +56 -0
- package/test-tools.js +19 -0
- package/test-unified-identity.js +3 -1
- package/test-usage-accounting.js +92 -20
- package/test-utility-provider-policy.js +486 -0
- package/web.js +130 -35
package/core/tool-guard.js
CHANGED
|
@@ -25,8 +25,39 @@ const CONFIG_DIR = require("../config-dir");
|
|
|
25
25
|
const GUARD_LOG = process.env.TOOL_GUARD_LOG || path.join(CONFIG_DIR, "tool-guard.jsonl");
|
|
26
26
|
const SETTINGS_FILE = process.env.DENY_GATE_SETTINGS || path.join(CONFIG_DIR, "deny-gate.settings.json");
|
|
27
27
|
|
|
28
|
-
// The authorised channels themselves must never be denied
|
|
29
|
-
|
|
28
|
+
// The authorised channels themselves must never be denied, but the exemption
|
|
29
|
+
// covers one complete outer shell command only. A prefix such as
|
|
30
|
+
// `open-claudia tool run ... && curl ...` must not bless the raw suffix, and
|
|
31
|
+
// command/process substitutions execute before the authorised CLI sees argv.
|
|
32
|
+
const AUTHORIZED_PREFIX_RE = /^\s*open-claudia\s+(?:tool\s+run|keyring\s+exec)\b/;
|
|
33
|
+
|
|
34
|
+
function isAuthorizedChannelCommand(command) {
|
|
35
|
+
const value = String(command || "");
|
|
36
|
+
if (!AUTHORIZED_PREFIX_RE.test(value)) return false;
|
|
37
|
+
let single = false;
|
|
38
|
+
let double = false;
|
|
39
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
40
|
+
const char = value[index];
|
|
41
|
+
if (char === "\\" && !single) {
|
|
42
|
+
index += 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (char === "'" && !double) {
|
|
46
|
+
single = !single;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (char === '"' && !single) {
|
|
50
|
+
double = !double;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (single) continue;
|
|
54
|
+
if (char === "`" || ((char === "$" || char === "<" || char === ">") && value[index + 1] === "(")) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (!double && /[;&|\r\n]/.test(char)) return false;
|
|
58
|
+
}
|
|
59
|
+
return !single && !double;
|
|
60
|
+
}
|
|
30
61
|
|
|
31
62
|
// An acting network/DB/shell-access binary at a command position (start of
|
|
32
63
|
// string or after a separator). Deliberately excludes read-only diagnosis
|
|
@@ -79,7 +110,7 @@ const RULES = [
|
|
|
79
110
|
function matchRule(command) {
|
|
80
111
|
const cmd = String(command || "");
|
|
81
112
|
if (!cmd.trim()) return null;
|
|
82
|
-
if (
|
|
113
|
+
if (isAuthorizedChannelCommand(cmd)) return null;
|
|
83
114
|
for (const rule of RULES) {
|
|
84
115
|
if (rule.raw) {
|
|
85
116
|
if (rule.raw.test(cmd)) return rule;
|
|
@@ -98,6 +129,30 @@ function denyMessage(rule) {
|
|
|
98
129
|
);
|
|
99
130
|
}
|
|
100
131
|
|
|
132
|
+
// Provider-neutral command policy. This function deliberately performs no
|
|
133
|
+
// payload parsing, configuration reads, logging, or process/exit translation;
|
|
134
|
+
// those are transport concerns owned by the Claude and Codex hook adapters.
|
|
135
|
+
function evaluateCommandPolicy(command, { mode = "strict" } = {}) {
|
|
136
|
+
if (mode !== "strict" && mode !== "relaxed") {
|
|
137
|
+
const error = new Error(`Invalid tool policy mode: ${mode}`);
|
|
138
|
+
error.code = "INVALID_TOOL_POLICY_MODE";
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
const rule = matchRule(command);
|
|
142
|
+
if (!rule) {
|
|
143
|
+
return { decision: "allow", ruleId: null, message: "", auditKind: null };
|
|
144
|
+
}
|
|
145
|
+
if (mode === "relaxed") {
|
|
146
|
+
return { decision: "allow", ruleId: rule.id, message: "", auditKind: "relaxed-pass" };
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
decision: "deny",
|
|
150
|
+
ruleId: rule.id,
|
|
151
|
+
message: denyMessage(rule),
|
|
152
|
+
auditKind: "deny",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
101
156
|
// Append-only audit trail — read by the 5c KPI pass. Best-effort: a logging
|
|
102
157
|
// failure must not turn into a denial or a crash.
|
|
103
158
|
function logGuardEvent(evt) {
|
|
@@ -141,7 +196,7 @@ const RAW_OP_PATTERNS = [
|
|
|
141
196
|
function matchRawOp(command) {
|
|
142
197
|
const cmd = String(command || "");
|
|
143
198
|
if (!cmd.trim()) return null;
|
|
144
|
-
if (
|
|
199
|
+
if (isAuthorizedChannelCommand(cmd)) return null;
|
|
145
200
|
if (matchRule(cmd)) return null;
|
|
146
201
|
for (const p of RAW_OP_PATTERNS) if (p.re.test(cmd)) return p;
|
|
147
202
|
return null;
|
|
@@ -191,57 +246,36 @@ function runHook(payload) {
|
|
|
191
246
|
try {
|
|
192
247
|
if (!data || data.tool_name !== "Bash") return { exit: 0, stderr: "" };
|
|
193
248
|
const command = data.tool_input && data.tool_input.command;
|
|
194
|
-
const rule = matchRule(command);
|
|
195
|
-
if (!rule) return { exit: 0, stderr: "" };
|
|
196
249
|
// Relaxed tooling mode: the gate observes but never blocks. The event is
|
|
197
250
|
// still logged (as relaxed-pass, not deny) so the dream KPI can report
|
|
198
251
|
// what WOULD have been denied — visibility survives the mode switch.
|
|
199
|
-
let
|
|
200
|
-
try {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
252
|
+
let mode = "strict";
|
|
253
|
+
try { mode = require("./tooling-mode").getToolingMode(); } catch (e) { /* fail-strict */ }
|
|
254
|
+
const policy = evaluateCommandPolicy(command, { mode });
|
|
255
|
+
if (policy.auditKind) {
|
|
256
|
+
logGuardEvent({ kind: policy.auditKind, rule: policy.ruleId, command: String(command).slice(0, 300) });
|
|
204
257
|
}
|
|
205
|
-
|
|
206
|
-
|
|
258
|
+
return policy.decision === "deny"
|
|
259
|
+
? { exit: 2, stderr: policy.message }
|
|
260
|
+
: { exit: 0, stderr: "" };
|
|
207
261
|
} catch (e) {
|
|
208
262
|
return { exit: 0, stderr: "" };
|
|
209
263
|
}
|
|
210
264
|
}
|
|
211
265
|
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
266
|
+
// Compatibility wrappers for callers that predate provider adapters. New code
|
|
267
|
+
// should use core/providers/claude-hook directly; delegating here keeps the old
|
|
268
|
+
// API without retaining a second, weaker settings writer.
|
|
215
269
|
function hookSettings() {
|
|
216
|
-
|
|
217
|
-
return {
|
|
218
|
-
hooks: {
|
|
219
|
-
PreToolUse: [
|
|
220
|
-
{
|
|
221
|
-
matcher: "Bash",
|
|
222
|
-
hooks: [
|
|
223
|
-
{ type: "command", command: `${JSON.stringify(process.execPath)} ${JSON.stringify(cli)} deny-gate-hook`, timeout: 10 },
|
|
224
|
-
],
|
|
225
|
-
},
|
|
226
|
-
],
|
|
227
|
-
},
|
|
228
|
-
};
|
|
270
|
+
return require("./providers/claude-hook").buildClaudeHookSettings();
|
|
229
271
|
}
|
|
230
272
|
|
|
231
|
-
// Write (or refresh) the settings file and return its path for --settings.
|
|
232
|
-
// Content-compared so repeated spawns don't churn the file. Throws only up to
|
|
233
|
-
// the caller's try/catch — a failure there just means no gate this turn.
|
|
234
273
|
function ensureHookSettings() {
|
|
235
|
-
|
|
236
|
-
try {
|
|
237
|
-
if (fs.readFileSync(SETTINGS_FILE, "utf-8") === body) return SETTINGS_FILE;
|
|
238
|
-
} catch (e) { /* missing/unreadable → write */ }
|
|
239
|
-
fs.writeFileSync(SETTINGS_FILE, body);
|
|
240
|
-
return SETTINGS_FILE;
|
|
274
|
+
return require("./providers/claude-hook").ensureClaudeHookSettings({ settingsFile: SETTINGS_FILE });
|
|
241
275
|
}
|
|
242
276
|
|
|
243
277
|
module.exports = {
|
|
244
278
|
RULES, RAW_OP_PATTERNS, GUARD_LOG, SETTINGS_FILE,
|
|
245
|
-
matchRule, denyMessage, logGuardEvent, runHook, hookSettings, ensureHookSettings,
|
|
279
|
+
isAuthorizedChannelCommand, matchRule, denyMessage, evaluateCommandPolicy, logGuardEvent, runHook, hookSettings, ensureHookSettings,
|
|
246
280
|
matchRawOp, noteRawOp, readGuardEvents,
|
|
247
281
|
};
|
package/core/tools.js
CHANGED
|
@@ -949,13 +949,26 @@ function runGate(tool, argv = [], env = process.env) {
|
|
|
949
949
|
|
|
950
950
|
// Env for running a tool: the bot's cred-free subprocess env plus ONLY the
|
|
951
951
|
// keyring keys this tool declares via --requires — least-privilege credential
|
|
952
|
-
// injection (5b).
|
|
953
|
-
//
|
|
954
|
-
//
|
|
952
|
+
// injection (5b). Provider/control credentials inherited by the Open Claudia
|
|
953
|
+
// CLI are stripped before declared keyring values are added, so a tool cannot
|
|
954
|
+
// accidentally inherit the active model provider's auth. Other ambient values
|
|
955
|
+
// retain the legacy process-env precedence. Keys are read fresh each run.
|
|
955
956
|
function runEnv(tool) {
|
|
956
957
|
let base;
|
|
957
958
|
try { base = require("./config").botSubprocessEnv(); }
|
|
958
959
|
catch (e) { base = { ...process.env }; }
|
|
960
|
+
try {
|
|
961
|
+
const { ALWAYS_STRIPPED_KEYS } = require("./providers/env");
|
|
962
|
+
for (const key of ALWAYS_STRIPPED_KEYS) delete base[key];
|
|
963
|
+
} catch (e) {
|
|
964
|
+
// The provider boundary is a required local module, but preserve a
|
|
965
|
+
// fail-closed fallback if a partial installation reaches this path.
|
|
966
|
+
for (const key of [
|
|
967
|
+
"CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "CURSOR_API_KEY",
|
|
968
|
+
"CLAUDE_CONFIG_DIR", "CODEX_HOME", "TELEGRAM_BOT_TOKEN", "KAZEE_BOT_TOKEN",
|
|
969
|
+
"BOT_CONTROL_TOKEN", "OC_SEND_TOKEN",
|
|
970
|
+
]) delete base[key];
|
|
971
|
+
}
|
|
959
972
|
const requires = (tool && tool.requires) || [];
|
|
960
973
|
if (requires.length) {
|
|
961
974
|
try {
|
|
@@ -973,10 +986,14 @@ function runEnv(tool) {
|
|
|
973
986
|
// by `tool show` to warn before a run fails on a missing credential.
|
|
974
987
|
function missingRequires(tool) {
|
|
975
988
|
if (!tool || !tool.requires || !tool.requires.length) return [];
|
|
989
|
+
let confined = new Set();
|
|
990
|
+
try { confined = require("./providers/env").ALWAYS_STRIPPED_KEYS; } catch (_) {}
|
|
976
991
|
let present;
|
|
977
992
|
try { present = new Set(require("./keyring").keys()); }
|
|
978
|
-
catch (e) { present = new Set(Object.keys(process.env)); }
|
|
979
|
-
return tool.requires.filter((
|
|
993
|
+
catch (e) { present = new Set(Object.keys(process.env).filter((key) => !confined.has(key))); }
|
|
994
|
+
return tool.requires.filter((key) => (
|
|
995
|
+
!present.has(key) && (confined.has(key) || process.env[key] === undefined)
|
|
996
|
+
));
|
|
980
997
|
}
|
|
981
998
|
|
|
982
999
|
// Recognise a Write/Edit aimed at a tool (for chat announcements). Legacy flat
|
package/core/transcripts.js
CHANGED
|
@@ -27,12 +27,37 @@ function transcriptProjectInfo(state = currentState()) {
|
|
|
27
27
|
return projectTranscripts.pointer(state.currentSession.dir, state.currentSession.name, state.userId);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// Build a read-only transcript view from the synchronously captured run
|
|
31
|
+
// identity. This does not migrate state; it only prevents an awaiting prompt
|
|
32
|
+
// build from following a later project/user switch.
|
|
33
|
+
function transcriptStateForRun(runContext, state = currentState()) {
|
|
34
|
+
if (!runContext) return state;
|
|
35
|
+
const project = runContext.project && typeof runContext.project === "object"
|
|
36
|
+
? runContext.project
|
|
37
|
+
: (runContext.project ? { name: runContext.project, dir: null } : null);
|
|
38
|
+
return {
|
|
39
|
+
...state,
|
|
40
|
+
userId: runContext.canonicalUserId || state.userId,
|
|
41
|
+
currentSession: project
|
|
42
|
+
? { name: project.name || null, dir: project.dir || project.path || null }
|
|
43
|
+
: null,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function transcriptProjectInfoForRun(runContext, state = currentState()) {
|
|
48
|
+
return transcriptProjectInfo(transcriptStateForRun(runContext, state));
|
|
49
|
+
}
|
|
50
|
+
|
|
30
51
|
function transcriptPointerNote(state = currentState()) {
|
|
31
52
|
if (!state.currentSession) return "";
|
|
32
53
|
return projectTranscripts.buildPointerNote(state.currentSession.dir, state.currentSession.name, state.userId);
|
|
33
54
|
}
|
|
34
55
|
|
|
35
|
-
function
|
|
56
|
+
function transcriptPointerNoteForRun(runContext, state = currentState()) {
|
|
57
|
+
return transcriptPointerNote(transcriptStateForRun(runContext, state));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function appendProjectTranscript(role, text, metadata = {}, state = currentState(), getActiveSessionId, options = {}) {
|
|
36
61
|
if (!state.currentSession) return null;
|
|
37
62
|
try {
|
|
38
63
|
const transport = (state.userId || "").split(":")[0] || "telegram";
|
|
@@ -54,6 +79,7 @@ function appendProjectTranscript(role, text, metadata = {}, state = currentState
|
|
|
54
79
|
return result;
|
|
55
80
|
} catch (e) {
|
|
56
81
|
console.error("Transcript write failed:", redactSensitive(e.message));
|
|
82
|
+
if (options.strict) throw e;
|
|
57
83
|
return null;
|
|
58
84
|
}
|
|
59
85
|
}
|
|
@@ -102,7 +128,10 @@ function stripTranscriptPointerForStorage(prompt) {
|
|
|
102
128
|
module.exports = {
|
|
103
129
|
projectTranscripts,
|
|
104
130
|
transcriptProjectInfo,
|
|
131
|
+
transcriptProjectInfoForRun,
|
|
105
132
|
transcriptPointerNote,
|
|
133
|
+
transcriptPointerNoteForRun,
|
|
134
|
+
transcriptStateForRun,
|
|
106
135
|
appendProjectTranscript,
|
|
107
136
|
recentTranscriptText,
|
|
108
137
|
promptWithTranscriptPointer,
|
package/core/usage-log.js
CHANGED
|
@@ -8,10 +8,24 @@ const CONFIG_DIR = require("../config-dir");
|
|
|
8
8
|
|
|
9
9
|
const USAGE_HISTORY_FILE = path.join(CONFIG_DIR, "usage-history.jsonl");
|
|
10
10
|
|
|
11
|
-
function
|
|
11
|
+
function normalizeUsageRecord(record) {
|
|
12
|
+
const normalized = { ...(record || {}) };
|
|
13
|
+
const provider = normalized.provider || normalized.backend || null;
|
|
14
|
+
if (provider) {
|
|
15
|
+
normalized.provider = provider;
|
|
16
|
+
normalized.backend = normalized.backend || provider;
|
|
17
|
+
}
|
|
18
|
+
return normalized;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function appendUsageRecord(record, options = {}) {
|
|
12
22
|
try {
|
|
13
|
-
fs.appendFileSync(USAGE_HISTORY_FILE, JSON.stringify(record) + "\n");
|
|
14
|
-
} catch (e) {
|
|
23
|
+
fs.appendFileSync(USAGE_HISTORY_FILE, JSON.stringify(normalizeUsageRecord(record)) + "\n");
|
|
24
|
+
} catch (e) {
|
|
25
|
+
if (options.strict) throw e;
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
15
29
|
}
|
|
16
30
|
|
|
17
31
|
function loadUsageHistory() {
|
|
@@ -21,7 +35,7 @@ function loadUsageHistory() {
|
|
|
21
35
|
for (const line of raw.split("\n")) {
|
|
22
36
|
const t = line.trim();
|
|
23
37
|
if (!t) continue;
|
|
24
|
-
try { out.push(JSON.parse(t)); } catch (e) {}
|
|
38
|
+
try { out.push(normalizeUsageRecord(JSON.parse(t))); } catch (e) {}
|
|
25
39
|
}
|
|
26
40
|
return out;
|
|
27
41
|
}
|
|
@@ -101,6 +115,7 @@ function evaluateUsageAlert(previousRecords, record, policy = usageAlertPolicy()
|
|
|
101
115
|
}
|
|
102
116
|
|
|
103
117
|
module.exports = {
|
|
118
|
+
normalizeUsageRecord,
|
|
104
119
|
appendUsageRecord,
|
|
105
120
|
loadUsageHistory,
|
|
106
121
|
usageAlertPolicy,
|