vibeusage 0.2.22 → 0.3.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.
@@ -0,0 +1,85 @@
1
+ const { createIntegrationContext } = require("./context");
2
+ const codex = require("./codex");
3
+ const everyCode = require("./every-code");
4
+ const claude = require("./claude");
5
+ const gemini = require("./gemini");
6
+ const opencode = require("./opencode");
7
+ const openclawSession = require("./openclaw-session");
8
+ const openclawLegacy = require("./openclaw-legacy");
9
+
10
+ const INTEGRATIONS = [
11
+ codex,
12
+ everyCode,
13
+ claude,
14
+ gemini,
15
+ opencode,
16
+ openclawSession,
17
+ openclawLegacy,
18
+ ];
19
+
20
+ function listIntegrations() {
21
+ return INTEGRATIONS.slice();
22
+ }
23
+
24
+ async function probeIntegrations(context) {
25
+ const ctx = await ensureContext(context);
26
+ const results = [];
27
+ for (const integration of INTEGRATIONS) {
28
+ results.push(await integration.probe(ctx));
29
+ }
30
+ return results;
31
+ }
32
+
33
+ async function installIntegrations(context) {
34
+ const ctx = await ensureContext(context);
35
+ const results = [];
36
+ for (const integration of INTEGRATIONS) {
37
+ results.push(await integration.install(ctx));
38
+ }
39
+ return results;
40
+ }
41
+
42
+ async function uninstallIntegrations(context) {
43
+ const ctx = await ensureContext(context);
44
+ const results = [];
45
+ for (const integration of INTEGRATIONS) {
46
+ results.push(await integration.uninstall(ctx));
47
+ }
48
+ return results;
49
+ }
50
+
51
+ function summarizeProbeForInitPreview(probe) {
52
+ switch (probe.status) {
53
+ case "ready":
54
+ return { label: probe.summaryLabel, status: "set", detail: "Already configured" };
55
+ case "not_installed":
56
+ if (probe.initPreviewStatus) {
57
+ return {
58
+ label: probe.summaryLabel,
59
+ status: probe.initPreviewStatus,
60
+ detail: probe.initPreviewDetail || probe.detail,
61
+ };
62
+ }
63
+ return { label: probe.summaryLabel, status: "skipped", detail: probe.detail };
64
+ case "unsupported_legacy":
65
+ return { label: probe.summaryLabel, status: "updated", detail: "Will replace legacy config" };
66
+ case "unreadable":
67
+ return { label: probe.summaryLabel, status: "skipped", detail: probe.detail || "Config unreadable" };
68
+ default:
69
+ return { label: probe.summaryLabel, status: "updated", detail: "Will reconcile config" };
70
+ }
71
+ }
72
+
73
+ async function ensureContext(context) {
74
+ if (context?.trackerPaths && context?.notifyPath) return context;
75
+ return createIntegrationContext(context);
76
+ }
77
+
78
+ module.exports = {
79
+ listIntegrations,
80
+ createIntegrationContext,
81
+ probeIntegrations,
82
+ installIntegrations,
83
+ uninstallIntegrations,
84
+ summarizeProbeForInitPreview,
85
+ };
@@ -0,0 +1,123 @@
1
+ const { probeOpenclawHookState, removeOpenclawHookConfig } = require("../openclaw-hook");
2
+
3
+ module.exports = {
4
+ name: "openclaw-legacy",
5
+ summaryLabel: "OpenClaw Hook (legacy)",
6
+ statusLabel: "OpenClaw hook (legacy)",
7
+ async probe(ctx) {
8
+ const state = await probeOpenclawHookState({
9
+ home: ctx.home,
10
+ trackerDir: ctx.trackerPaths.trackerDir,
11
+ env: ctx.env,
12
+ });
13
+ if (state?.skippedReason === "openclaw-config-missing") {
14
+ return baseProbe(this, { status: "not_installed", detail: "OpenClaw config not found" });
15
+ }
16
+ if (state?.skippedReason === "openclaw-config-unreadable") {
17
+ return baseProbe(this, {
18
+ status: "unreadable",
19
+ detail: state.error
20
+ ? `OpenClaw config unreadable: ${state.error}`
21
+ : "OpenClaw config unreadable",
22
+ linked: Boolean(state.linked),
23
+ enabled: Boolean(state.enabled),
24
+ });
25
+ }
26
+ if (state?.configured || state?.linked || state?.enabled) {
27
+ return baseProbe(this, {
28
+ status: "unsupported_legacy",
29
+ detail: "Legacy OpenClaw hook detected; run vibeusage init",
30
+ linked: Boolean(state.linked),
31
+ enabled: Boolean(state.enabled),
32
+ });
33
+ }
34
+ return baseProbe(this, { status: "not_installed", detail: "Legacy hook not installed" });
35
+ },
36
+ async install(ctx) {
37
+ const state = await probeOpenclawHookState({
38
+ home: ctx.home,
39
+ trackerDir: ctx.trackerPaths.trackerDir,
40
+ env: ctx.env,
41
+ });
42
+ if (state?.skippedReason === "openclaw-config-unreadable") {
43
+ return action(
44
+ this,
45
+ "skipped",
46
+ false,
47
+ state.error ? `OpenClaw config unreadable: ${state.error}` : "OpenClaw config unreadable",
48
+ { skippedReason: state.skippedReason },
49
+ );
50
+ }
51
+ if (!(state?.configured || state?.linked || state?.enabled)) {
52
+ return action(this, "unchanged", false, "no change");
53
+ }
54
+ const result = await removeOpenclawHookConfig({
55
+ home: ctx.home,
56
+ trackerDir: ctx.trackerPaths.trackerDir,
57
+ env: ctx.env,
58
+ });
59
+ if (result?.removed) {
60
+ return action(this, "updated", true, "Removed legacy command hook");
61
+ }
62
+ if (result?.skippedReason === "openclaw-config-unreadable") {
63
+ return action(
64
+ this,
65
+ "skipped",
66
+ false,
67
+ result.error ? `OpenClaw config unreadable: ${result.error}` : "OpenClaw config unreadable",
68
+ { skippedReason: result.skippedReason },
69
+ );
70
+ }
71
+ return action(this, "unchanged", false, "no change");
72
+ },
73
+ async uninstall(ctx) {
74
+ const result = await removeOpenclawHookConfig({
75
+ home: ctx.home,
76
+ trackerDir: ctx.trackerPaths.trackerDir,
77
+ env: ctx.env,
78
+ });
79
+ if (result?.removed) {
80
+ return action(this, "removed", true, result.openclawConfigPath);
81
+ }
82
+ if (result?.skippedReason === "openclaw-config-missing") {
83
+ return action(this, "skipped", false, "openclaw config not found", {
84
+ skippedReason: result.skippedReason,
85
+ });
86
+ }
87
+ if (result?.skippedReason === "openclaw-config-unreadable") {
88
+ return action(
89
+ this,
90
+ "skipped",
91
+ false,
92
+ result.error ? `openclaw config unreadable: ${result.error}` : "openclaw config unreadable",
93
+ { skippedReason: result.skippedReason },
94
+ );
95
+ }
96
+ return action(this, "unchanged", false, "no change");
97
+ },
98
+ renderStatusValue(probe) {
99
+ if (probe.status === "not_installed") return "unset";
100
+ return probe.status;
101
+ },
102
+ };
103
+
104
+ function baseProbe(descriptor, values) {
105
+ return {
106
+ name: descriptor.name,
107
+ summaryLabel: descriptor.summaryLabel,
108
+ statusLabel: descriptor.statusLabel,
109
+ configured: false,
110
+ ...values,
111
+ };
112
+ }
113
+
114
+ function action(descriptor, status, changed, detail, extras = {}) {
115
+ return {
116
+ name: descriptor.name,
117
+ label: descriptor.summaryLabel,
118
+ status,
119
+ changed,
120
+ detail,
121
+ ...extras,
122
+ };
123
+ }
@@ -0,0 +1,132 @@
1
+ const {
2
+ installOpenclawSessionPlugin,
3
+ probeOpenclawSessionPluginState,
4
+ removeOpenclawSessionPluginConfig,
5
+ } = require("../openclaw-session-plugin");
6
+
7
+ module.exports = {
8
+ name: "openclaw-session",
9
+ summaryLabel: "OpenClaw Session Plugin",
10
+ statusLabel: "OpenClaw session plugin",
11
+ async probe(ctx) {
12
+ const state = await probeOpenclawSessionPluginState({
13
+ home: ctx.home,
14
+ trackerDir: ctx.trackerPaths.trackerDir,
15
+ env: ctx.env,
16
+ });
17
+ if (state?.skippedReason === "openclaw-config-missing") {
18
+ return baseProbe(this, { status: "not_installed", detail: "OpenClaw config not found" });
19
+ }
20
+ if (state?.skippedReason === "openclaw-config-unreadable") {
21
+ return baseProbe(this, {
22
+ status: "unreadable",
23
+ detail: state.error
24
+ ? `OpenClaw config unreadable: ${state.error}`
25
+ : "OpenClaw config unreadable",
26
+ linked: Boolean(state.linked),
27
+ enabled: Boolean(state.enabled),
28
+ });
29
+ }
30
+ if (state?.configured) {
31
+ return baseProbe(this, {
32
+ status: "ready",
33
+ detail: "Session plugin linked",
34
+ configured: true,
35
+ linked: Boolean(state.linked),
36
+ enabled: Boolean(state.enabled),
37
+ });
38
+ }
39
+ return baseProbe(this, {
40
+ status: "drifted",
41
+ detail: "Run vibeusage init to reconcile session plugin",
42
+ linked: Boolean(state?.linked),
43
+ enabled: Boolean(state?.enabled),
44
+ });
45
+ },
46
+ async install(ctx) {
47
+ const before = await probeOpenclawSessionPluginState({
48
+ home: ctx.home,
49
+ trackerDir: ctx.trackerPaths.trackerDir,
50
+ env: ctx.env,
51
+ });
52
+ const result = await installOpenclawSessionPlugin({
53
+ home: ctx.home,
54
+ trackerDir: ctx.trackerPaths.trackerDir,
55
+ packageName: "vibeusage",
56
+ env: ctx.env,
57
+ });
58
+ if (result?.skippedReason === "openclaw-cli-missing") {
59
+ return action(this, "skipped", false, "OpenClaw CLI not found");
60
+ }
61
+ if (result?.skippedReason === "openclaw-plugins-install-failed") {
62
+ return action(this, "skipped", false, `Install failed: ${result.error || "unknown error"}`);
63
+ }
64
+ if (result?.skippedReason === "openclaw-config-unreadable") {
65
+ return action(
66
+ this,
67
+ "skipped",
68
+ false,
69
+ result.error ? `OpenClaw config unreadable: ${result.error}` : "OpenClaw config unreadable",
70
+ );
71
+ }
72
+ return action(
73
+ this,
74
+ before?.configured ? "set" : "installed",
75
+ Boolean(!before?.configured && result?.configured),
76
+ before?.configured
77
+ ? "Session plugin already linked"
78
+ : "Session plugin linked (restart OpenClaw gateway to activate)",
79
+ );
80
+ },
81
+ async uninstall(ctx) {
82
+ const result = await removeOpenclawSessionPluginConfig({
83
+ home: ctx.home,
84
+ trackerDir: ctx.trackerPaths.trackerDir,
85
+ env: ctx.env,
86
+ });
87
+ if (result?.removed) {
88
+ return action(this, "removed", true, result.openclawConfigPath);
89
+ }
90
+ if (result?.skippedReason === "openclaw-config-missing") {
91
+ return action(this, "skipped", false, "openclaw config not found", {
92
+ skippedReason: result.skippedReason,
93
+ });
94
+ }
95
+ if (result?.skippedReason === "openclaw-config-unreadable") {
96
+ return action(
97
+ this,
98
+ "skipped",
99
+ false,
100
+ result.error ? `openclaw config unreadable: ${result.error}` : "openclaw config unreadable",
101
+ { skippedReason: result.skippedReason },
102
+ );
103
+ }
104
+ return action(this, "unchanged", false, "no change");
105
+ },
106
+ renderStatusValue(probe) {
107
+ if (probe.status === "ready") return "set";
108
+ if (probe.status === "not_installed") return "unset";
109
+ return probe.status;
110
+ },
111
+ };
112
+
113
+ function baseProbe(descriptor, values) {
114
+ return {
115
+ name: descriptor.name,
116
+ summaryLabel: descriptor.summaryLabel,
117
+ statusLabel: descriptor.statusLabel,
118
+ configured: false,
119
+ ...values,
120
+ };
121
+ }
122
+
123
+ function action(descriptor, status, changed, detail, extras = {}) {
124
+ return {
125
+ name: descriptor.name,
126
+ label: descriptor.summaryLabel,
127
+ status,
128
+ changed,
129
+ detail,
130
+ ...extras,
131
+ };
132
+ }
@@ -0,0 +1,86 @@
1
+ const { isOpencodePluginInstalled, upsertOpencodePlugin, removeOpencodePlugin } = require("../opencode-config");
2
+ const { isDir } = require("./utils");
3
+
4
+ module.exports = {
5
+ name: "opencode",
6
+ summaryLabel: "Opencode Plugin",
7
+ statusLabel: "Opencode plugin",
8
+ async probe(ctx) {
9
+ const hasConfigDir = await isDir(ctx.opencode.configDir);
10
+ if (!hasConfigDir) {
11
+ return baseProbe(this, {
12
+ status: "not_installed",
13
+ detail: "Config not found",
14
+ initPreviewStatus: "updated",
15
+ initPreviewDetail: "Will install plugin",
16
+ });
17
+ }
18
+ const configured = await isOpencodePluginInstalled({ configDir: ctx.opencode.configDir });
19
+ return baseProbe(this, {
20
+ status: configured ? "ready" : "drifted",
21
+ detail: configured ? "Plugin installed" : "Run vibeusage init to reconcile plugin",
22
+ configured,
23
+ });
24
+ },
25
+ async install(ctx) {
26
+ const result = await upsertOpencodePlugin({
27
+ configDir: ctx.opencode.configDir,
28
+ notifyPath: ctx.notifyPath,
29
+ });
30
+ if (result?.skippedReason === "config-missing") {
31
+ return action(this, "skipped", false, "Config not found");
32
+ }
33
+ return action(
34
+ this,
35
+ result.changed ? "installed" : "set",
36
+ Boolean(result.changed),
37
+ result.changed ? "Plugin installed" : "Plugin already installed",
38
+ );
39
+ },
40
+ async uninstall(ctx) {
41
+ if (!(await isDir(ctx.opencode.configDir))) {
42
+ return action(this, "skipped", false, "config dir not found");
43
+ }
44
+ const result = await removeOpencodePlugin({ configDir: ctx.opencode.configDir });
45
+ if (result.removed) {
46
+ return action(this, "removed", true, ctx.opencode.configDir);
47
+ }
48
+ if (result.skippedReason === "plugin-missing") {
49
+ return action(this, "unchanged", false, "no change", {
50
+ skippedReason: result.skippedReason,
51
+ });
52
+ }
53
+ if (result.skippedReason === "unexpected-content") {
54
+ return action(this, "skipped", false, "unexpected content", {
55
+ skippedReason: result.skippedReason,
56
+ });
57
+ }
58
+ return action(this, "skipped", false, "config dir not found");
59
+ },
60
+ renderStatusValue(probe) {
61
+ if (probe.status === "ready") return "set";
62
+ if (probe.status === "not_installed") return "unset";
63
+ return probe.status;
64
+ },
65
+ };
66
+
67
+ function baseProbe(descriptor, values) {
68
+ return {
69
+ name: descriptor.name,
70
+ summaryLabel: descriptor.summaryLabel,
71
+ statusLabel: descriptor.statusLabel,
72
+ configured: false,
73
+ ...values,
74
+ };
75
+ }
76
+
77
+ function action(descriptor, status, changed, detail, extras = {}) {
78
+ return {
79
+ name: descriptor.name,
80
+ label: descriptor.summaryLabel,
81
+ status,
82
+ changed,
83
+ detail,
84
+ ...extras,
85
+ };
86
+ }
@@ -0,0 +1,39 @@
1
+ const fs = require("node:fs/promises");
2
+
3
+ async function isFile(targetPath) {
4
+ try {
5
+ const stat = await fs.stat(targetPath);
6
+ return stat.isFile();
7
+ } catch (_err) {
8
+ return false;
9
+ }
10
+ }
11
+
12
+ async function isDir(targetPath) {
13
+ try {
14
+ const stat = await fs.stat(targetPath);
15
+ return stat.isDirectory();
16
+ } catch (_err) {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ function arraysEqual(left, right) {
22
+ if (!Array.isArray(left) || !Array.isArray(right)) return false;
23
+ if (left.length !== right.length) return false;
24
+ for (let i = 0; i < left.length; i += 1) {
25
+ if (String(left[i]) !== String(right[i])) return false;
26
+ }
27
+ return true;
28
+ }
29
+
30
+ function findIntegration(results, name) {
31
+ return Array.isArray(results) ? results.find((entry) => entry?.name === name) || null : null;
32
+ }
33
+
34
+ module.exports = {
35
+ isFile,
36
+ isDir,
37
+ arraysEqual,
38
+ findIntegration,
39
+ };
@@ -1,6 +1,8 @@
1
- const DEFAULT_BASE_URL = "https://5tmappuk.us-east.insforge.app";
2
- const DEFAULT_DASHBOARD_URL = "https://www.vibeusage.cc";
3
- const DEFAULT_HTTP_TIMEOUT_MS = 20_000;
1
+ const {
2
+ DEFAULT_INSFORGE_BASE_URL,
3
+ DEFAULT_DASHBOARD_URL,
4
+ DEFAULT_HTTP_TIMEOUT_MS,
5
+ } = require("../shared/runtime-defaults.cjs");
4
6
 
5
7
  function resolveRuntimeConfig({ cli = {}, config = {}, env = process.env, defaults = {} } = {}) {
6
8
  const baseUrl = pickString(
@@ -8,7 +10,7 @@ function resolveRuntimeConfig({ cli = {}, config = {}, env = process.env, defaul
8
10
  config.baseUrl,
9
11
  env?.VIBEUSAGE_INSFORGE_BASE_URL,
10
12
  defaults.baseUrl,
11
- DEFAULT_BASE_URL,
13
+ DEFAULT_INSFORGE_BASE_URL,
12
14
  );
13
15
  const dashboardUrl = pickString(
14
16
  cli.dashboardUrl,
@@ -125,7 +127,7 @@ function clampInt(value, min, max) {
125
127
  }
126
128
 
127
129
  module.exports = {
128
- DEFAULT_BASE_URL,
130
+ DEFAULT_BASE_URL: DEFAULT_INSFORGE_BASE_URL,
129
131
  DEFAULT_DASHBOARD_URL,
130
132
  DEFAULT_HTTP_TIMEOUT_MS,
131
133
  resolveRuntimeConfig,
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
2
 
3
3
  const { createInsforgeClient } = require("./insforge-client");
4
+ const {
5
+ BACKEND_RUNTIME_UNAVAILABLE_MESSAGE,
6
+ FUNCTION_SLUGS,
7
+ } = require("../shared/vibeusage-function-contract.cjs");
4
8
 
5
9
  async function signInWithPassword({ baseUrl, email, password }) {
6
10
  const client = createInsforgeClient({ baseUrl });
@@ -19,7 +23,7 @@ async function issueDeviceToken({ baseUrl, accessToken, deviceName, platform = "
19
23
  const data = await invokeFunction({
20
24
  baseUrl,
21
25
  accessToken,
22
- slug: "vibeusage-device-token-issue",
26
+ slug: FUNCTION_SLUGS.deviceTokenIssue,
23
27
  method: "POST",
24
28
  body: { device_name: deviceName, platform },
25
29
  errorPrefix: "Device token issue failed",
@@ -40,7 +44,7 @@ async function exchangeLinkCode({ baseUrl, linkCode, requestId, deviceName, plat
40
44
  const data = await invokeFunction({
41
45
  baseUrl,
42
46
  accessToken: null,
43
- slug: "vibeusage-link-code-exchange",
47
+ slug: FUNCTION_SLUGS.linkCodeExchange,
44
48
  method: "POST",
45
49
  body: {
46
50
  link_code: linkCode,
@@ -78,7 +82,7 @@ async function ingestHourly({
78
82
  const data = await invokeFunctionWithRetry({
79
83
  baseUrl,
80
84
  accessToken: deviceToken,
81
- slug: "vibeusage-ingest",
85
+ slug: FUNCTION_SLUGS.ingest,
82
86
  method: "POST",
83
87
  body,
84
88
  errorPrefix: "Ingest failed",
@@ -95,7 +99,7 @@ async function syncHeartbeat({ baseUrl, deviceToken }) {
95
99
  const data = await invokeFunction({
96
100
  baseUrl,
97
101
  accessToken: deviceToken,
98
- slug: "vibeusage-sync-ping",
102
+ slug: FUNCTION_SLUGS.syncPing,
99
103
  method: "POST",
100
104
  body: {},
101
105
  errorPrefix: "Sync heartbeat failed",
@@ -174,7 +178,7 @@ function extractSdkErrorMessage(error) {
174
178
 
175
179
  function normalizeBackendErrorMessage(message) {
176
180
  if (!isBackendRuntimeDownMessage(message)) return String(message || "Unknown error");
177
- return "Backend runtime unavailable (InsForge). Please retry later.";
181
+ return BACKEND_RUNTIME_UNAVAILABLE_MESSAGE;
178
182
  }
179
183
 
180
184
  function isBackendRuntimeDownMessage(message) {
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+
3
+ const REQUIRED_COPY_COLUMNS = ["key", "module", "page", "component", "slot", "text"];
4
+
5
+ function parseCsvRows(raw) {
6
+ const rows = [];
7
+ let row = [];
8
+ let field = "";
9
+ let inQuotes = false;
10
+
11
+ for (let i = 0; i < String(raw || "").length; i += 1) {
12
+ const ch = raw[i];
13
+
14
+ if (inQuotes) {
15
+ if (ch === '"') {
16
+ const next = raw[i + 1];
17
+ if (next === '"') {
18
+ field += '"';
19
+ i += 1;
20
+ } else {
21
+ inQuotes = false;
22
+ }
23
+ } else {
24
+ field += ch;
25
+ }
26
+ continue;
27
+ }
28
+
29
+ if (ch === '"') {
30
+ inQuotes = true;
31
+ continue;
32
+ }
33
+
34
+ if (ch === ",") {
35
+ row.push(field);
36
+ field = "";
37
+ continue;
38
+ }
39
+
40
+ if (ch === "\n") {
41
+ row.push(field);
42
+ field = "";
43
+ if (!row.every((cell) => String(cell).trim() === "")) {
44
+ rows.push(row);
45
+ }
46
+ row = [];
47
+ continue;
48
+ }
49
+
50
+ if (ch === "\r") continue;
51
+
52
+ field += ch;
53
+ }
54
+
55
+ row.push(field);
56
+ if (!row.every((cell) => String(cell).trim() === "")) {
57
+ rows.push(row);
58
+ }
59
+
60
+ return rows;
61
+ }
62
+
63
+ function buildCopyRegistry(raw) {
64
+ const rows = parseCsvRows(raw || "");
65
+ if (!rows.length) {
66
+ return {
67
+ header: [],
68
+ rows: [],
69
+ map: new Map(),
70
+ duplicates: new Map(),
71
+ missingColumns: [...REQUIRED_COPY_COLUMNS],
72
+ };
73
+ }
74
+
75
+ const header = rows[0].map((cell) => String(cell).trim());
76
+ const missingColumns = REQUIRED_COPY_COLUMNS.filter((col) => !header.includes(col));
77
+ if (missingColumns.length > 0) {
78
+ return {
79
+ header,
80
+ rows: [],
81
+ map: new Map(),
82
+ duplicates: new Map(),
83
+ missingColumns,
84
+ };
85
+ }
86
+
87
+ const indexByColumn = Object.fromEntries(header.map((col, index) => [col, index]));
88
+ const entries = [];
89
+ const map = new Map();
90
+ const duplicates = new Map();
91
+
92
+ rows.slice(1).forEach((cells, rowIndex) => {
93
+ const record = {
94
+ key: String(cells[indexByColumn.key] || "").trim(),
95
+ module: String(cells[indexByColumn.module] || "").trim(),
96
+ page: String(cells[indexByColumn.page] || "").trim(),
97
+ component: String(cells[indexByColumn.component] || "").trim(),
98
+ slot: String(cells[indexByColumn.slot] || "").trim(),
99
+ text: String(cells[indexByColumn.text] ?? "").trim(),
100
+ row: rowIndex + 2,
101
+ };
102
+
103
+ if (!record.key) return;
104
+
105
+ if (map.has(record.key)) {
106
+ const existingRows = duplicates.get(record.key) || [map.get(record.key).row];
107
+ existingRows.push(record.row);
108
+ duplicates.set(record.key, existingRows);
109
+ }
110
+
111
+ map.set(record.key, record);
112
+ entries.push(record);
113
+ });
114
+
115
+ return {
116
+ header,
117
+ rows: entries,
118
+ map,
119
+ duplicates,
120
+ missingColumns: [],
121
+ };
122
+ }
123
+
124
+ function normalizeCopyText(text) {
125
+ return String(text ?? "").replace(/\\n/g, "\n");
126
+ }
127
+
128
+ function interpolateCopyText(text, params) {
129
+ if (!params || typeof params !== "object") return text;
130
+ return String(text).replace(/\{\{(\w+)\}\}/g, (match, key) => {
131
+ if (params[key] == null) return match;
132
+ return String(params[key]);
133
+ });
134
+ }
135
+
136
+ module.exports = {
137
+ REQUIRED_COPY_COLUMNS,
138
+ parseCsvRows,
139
+ buildCopyRegistry,
140
+ normalizeCopyText,
141
+ interpolateCopyText,
142
+ };