@tenonhq/dovetail-core 0.0.125 → 0.0.127

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,35 @@
1
+ "use strict";
2
+ // snClient.createRecord refuses tables the generic createRecord op cannot
3
+ // write correctly. sys_update_set: the op never sets `application`, so the set
4
+ // would land in the session app — callers must use createUpdateSet instead.
5
+ // The refusal happens before any HTTP call.
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ jest.mock("../Logger", function () {
8
+ return {
9
+ logger: {
10
+ info: jest.fn(),
11
+ debug: jest.fn(),
12
+ warn: jest.fn(),
13
+ error: jest.fn(),
14
+ getLogLevel: function () { return "debug"; },
15
+ },
16
+ };
17
+ });
18
+ jest.mock("../FileLogger", function () {
19
+ return { fileLogger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() } };
20
+ });
21
+ const snClient_1 = require("../snClient");
22
+ describe("snClient.createRecord guards", function () {
23
+ it("rejects sys_update_set without posting, pointing at dove createUpdateSet", async function () {
24
+ var c = (0, snClient_1.snClient)("https://x.service-now.com/", "u", "p");
25
+ var post = jest.spyOn(c.client, "post");
26
+ await expect(c.createRecord({ table: "sys_update_set", fields: { name: "Foo" }, scope: "x_s" })).rejects.toThrow(/dove createUpdateSet/);
27
+ expect(post).not.toHaveBeenCalled();
28
+ });
29
+ it("still rejects sys_db_object without posting", async function () {
30
+ var c = (0, snClient_1.snClient)("https://x.service-now.com/", "u", "p");
31
+ var post = jest.spyOn(c.client, "post");
32
+ await expect(c.createRecord({ table: "sys_db_object", fields: { name: "x_t" } })).rejects.toThrow(/sys_db_object/);
33
+ expect(post).not.toHaveBeenCalled();
34
+ });
35
+ });
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ // snClient.getScopeId must resolve a scope name to exactly one sys_scope row.
3
+ // `scope=global` also matches sys_app rows on live instances, so Global is
4
+ // addressed by its fixed sys_id; any other name with >1 row is refused.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ jest.mock("../Logger", function () {
7
+ return {
8
+ logger: {
9
+ info: jest.fn(),
10
+ debug: jest.fn(),
11
+ warn: jest.fn(),
12
+ error: jest.fn(),
13
+ getLogLevel: function () { return "debug"; },
14
+ },
15
+ };
16
+ });
17
+ jest.mock("../FileLogger", function () {
18
+ return { fileLogger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() } };
19
+ });
20
+ const snClient_1 = require("../snClient");
21
+ // Rows keyed by the encoded query the client sends.
22
+ function stubGet(c, rowsByQuery) {
23
+ var queries = [];
24
+ jest.spyOn(c.client, "get").mockImplementation(async function (_url, config) {
25
+ var params = (config && config.params);
26
+ var q = (params && params.sysparm_query) || "";
27
+ queries.push(q);
28
+ return {
29
+ status: 200,
30
+ statusText: "OK",
31
+ headers: {},
32
+ config: {},
33
+ data: { result: rowsByQuery[q] || [] },
34
+ };
35
+ });
36
+ return queries;
37
+ }
38
+ describe("snClient.getScopeId", function () {
39
+ it("resolves global to the real Global scope even when sys_app rows share scope=global", async function () {
40
+ var c = (0, snClient_1.snClient)("https://x.service-now.com/", "u", "p");
41
+ var queries = stubGet(c, {
42
+ "scope=global": [{ sys_id: "489827aabbccdd" }, { sys_id: "global" }],
43
+ "sys_id=global": [{ sys_id: "global" }],
44
+ });
45
+ var rows = await (0, snClient_1.unwrapSNResponse)(c.getScopeId("global"));
46
+ expect(rows).toHaveLength(1);
47
+ expect(rows[0].sys_id).toBe("global");
48
+ expect(queries).toEqual(["sys_id=global"]);
49
+ });
50
+ it("throws when a non-global scope name matches more than one row", async function () {
51
+ var c = (0, snClient_1.snClient)("https://x.service-now.com/", "u", "p");
52
+ stubGet(c, { "scope=x_dup": [{ sys_id: "a1" }, { sys_id: "b2" }] });
53
+ await expect(c.getScopeId("x_dup")).rejects.toThrow(/ambiguous.*a1, b2/);
54
+ });
55
+ it("returns the single row for an unambiguous scope", async function () {
56
+ var c = (0, snClient_1.snClient)("https://x.service-now.com/", "u", "p");
57
+ stubGet(c, { "scope=x_one": [{ sys_id: "s1" }] });
58
+ var rows = await (0, snClient_1.unwrapSNResponse)(c.getScopeId("x_one"));
59
+ expect(rows).toEqual([{ sys_id: "s1" }]);
60
+ });
61
+ it("returns no rows for an unknown scope (callers handle the empty case)", async function () {
62
+ var c = (0, snClient_1.snClient)("https://x.service-now.com/", "u", "p");
63
+ stubGet(c, {});
64
+ var rows = await (0, snClient_1.unwrapSNResponse)(c.getScopeId("x_missing"));
65
+ expect(rows).toEqual([]);
66
+ });
67
+ });
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ // `dove switchUpdateSet --sysId <id>` selects the in-progress set by exact
3
+ // sys_id, so two same-named sets can't be confused. Invalid ids are refused
4
+ // before any query; an id with no in-progress match is an error, not a prompt.
5
+ var __importDefault = (this && this.__importDefault) || function (mod) {
6
+ return (mod && mod.__esModule) ? mod : { "default": mod };
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ var mockLogger = {
10
+ info: jest.fn(),
11
+ error: jest.fn(),
12
+ warn: jest.fn(),
13
+ debug: jest.fn(),
14
+ success: jest.fn(),
15
+ getLogLevel: jest.fn().mockReturnValue("info"),
16
+ };
17
+ jest.mock("../Logger", function () {
18
+ return { logger: mockLogger };
19
+ });
20
+ jest.mock("../commands", function () {
21
+ return { setLogLevel: jest.fn() };
22
+ });
23
+ var mockWriteRouting = jest.fn().mockReturnValue(true);
24
+ jest.mock("../updateSetConfig", function () {
25
+ return { writeUpdateSetRouting: mockWriteRouting };
26
+ });
27
+ jest.mock("inquirer", function () {
28
+ return { prompt: jest.fn() };
29
+ });
30
+ var TARGET_ID = "0123456789abcdef0123456789abcdef";
31
+ var SCOPE_SYS_ID = "scopeSysId0000000000000000000001";
32
+ var mockGet = jest.fn();
33
+ var mockClient = {
34
+ getScopeId: jest.fn(),
35
+ changeUpdateSet: jest.fn(),
36
+ getCurrentUpdateSet: jest.fn(),
37
+ getScopeById: jest.fn(),
38
+ client: { get: mockGet },
39
+ };
40
+ jest.mock("../snClient", function () {
41
+ return {
42
+ defaultClient: jest.fn(function () { return mockClient; }),
43
+ unwrapSNResponse: jest.fn(async function (p) {
44
+ var resp = await p;
45
+ return resp.data.result;
46
+ }),
47
+ unwrapTableAPIFirstItem: jest.fn(),
48
+ };
49
+ });
50
+ const inquirer_1 = __importDefault(require("inquirer"));
51
+ const updateSetCommands_1 = require("../updateSetCommands");
52
+ function axiosResult(result) {
53
+ return Promise.resolve({ data: { result: result } });
54
+ }
55
+ describe("switchUpdateSet --sysId", function () {
56
+ beforeEach(function () {
57
+ jest.clearAllMocks();
58
+ mockClient.getScopeId.mockReturnValue(axiosResult([{ sys_id: SCOPE_SYS_ID }]));
59
+ mockClient.changeUpdateSet.mockResolvedValue({ data: { result: { message: "Success" } } });
60
+ mockClient.getCurrentUpdateSet.mockResolvedValue({ data: { result: { sysId: TARGET_ID } } });
61
+ });
62
+ it("queries by exact sys_id within the scope and switches to that set", async function () {
63
+ mockGet.mockReturnValue(axiosResult([{ sys_id: TARGET_ID, name: "Dup Name", application: { value: SCOPE_SYS_ID, display_value: "App" } }]));
64
+ await (0, updateSetCommands_1.switchUpdateSetCommand)({ sysId: TARGET_ID, scope: "x_app", logLevel: "info" });
65
+ expect(mockGet).toHaveBeenCalledTimes(1);
66
+ var query = mockGet.mock.calls[0][1].params.sysparm_query;
67
+ expect(query).toBe("state=in progress^application=" + SCOPE_SYS_ID + "^sys_id=" + TARGET_ID);
68
+ expect(mockClient.changeUpdateSet).toHaveBeenCalledWith(expect.objectContaining({ sysId: TARGET_ID }));
69
+ expect(mockWriteRouting).toHaveBeenCalledWith({ scope: "x_app", sysId: TARGET_ID, name: "Dup Name" });
70
+ expect(inquirer_1.default.prompt).not.toHaveBeenCalled();
71
+ });
72
+ it("refuses a malformed sys_id before querying", async function () {
73
+ await expect((0, updateSetCommands_1.switchUpdateSetCommand)({ sysId: "abc^name=x", scope: "x_app", logLevel: "info" })).rejects.toThrow(/No update set selected/);
74
+ expect(mockGet).not.toHaveBeenCalled();
75
+ expect(mockClient.changeUpdateSet).not.toHaveBeenCalled();
76
+ });
77
+ it("errors (no prompt, no switch) when no in-progress set has that sys_id", async function () {
78
+ mockGet.mockReturnValue(axiosResult([]));
79
+ await expect((0, updateSetCommands_1.switchUpdateSetCommand)({ sysId: TARGET_ID, scope: "x_app", logLevel: "info" })).rejects.toThrow(/No update set selected/);
80
+ expect(mockClient.changeUpdateSet).not.toHaveBeenCalled();
81
+ expect(inquirer_1.default.prompt).not.toHaveBeenCalled();
82
+ });
83
+ });
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ // Handler-level tests for the Claude Code guard on the human-only watcher
3
+ // (watchAllScopesCommand, TenonHQ/Dovetail#155):
4
+ // - CLAUDECODE set -> exit 1 before the watcher starts;
5
+ // - CLAUDECODE + override -> warns, starts;
6
+ // - only a CLAUDE_CODE_* config variable (a human shell) -> warns, starts.
7
+ // The watcher module is mocked, so nothing is watched or pushed.
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ var mockLogger = {
10
+ info: jest.fn(),
11
+ debug: jest.fn(),
12
+ warn: jest.fn(),
13
+ error: jest.fn(),
14
+ success: jest.fn(),
15
+ getLogLevel: function () { return "info"; },
16
+ };
17
+ jest.mock("../Logger", function () { return { logger: mockLogger }; });
18
+ jest.mock("../FileLogger", function () {
19
+ return { fileLogger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() } };
20
+ });
21
+ jest.mock("../config", function () { return {}; });
22
+ jest.mock("../snClient", function () {
23
+ return {
24
+ defaultClient: function () { return {}; },
25
+ unwrapSNResponse: function (p) { return Promise.resolve(p); },
26
+ };
27
+ });
28
+ jest.mock("../wizard", function () {
29
+ return { setupDotEnv: jest.fn(), getLoginInfo: jest.fn() };
30
+ });
31
+ jest.mock("../commands", function () { return { setLogLevel: jest.fn() }; });
32
+ var mockStartWatching = jest.fn().mockResolvedValue(undefined);
33
+ jest.mock("../MultiScopeWatcher", function () {
34
+ return { startMultiScopeWatching: mockStartWatching, stopMultiScopeWatching: jest.fn() };
35
+ });
36
+ const allScopesCommands_1 = require("../allScopesCommands");
37
+ const claudeSession_1 = require("../claudeSession");
38
+ class ExitSignal extends Error {
39
+ code;
40
+ constructor(code) {
41
+ super("process.exit(" + code + ")");
42
+ this.code = code;
43
+ }
44
+ }
45
+ var ARGS = { logLevel: "info", noDashboard: true, noMonitoring: true };
46
+ // Every env key this suite touches. The suite may itself run inside a Claude
47
+ // Code shell, so the Claude markers are cleared before each test.
48
+ function isManagedKey(key) {
49
+ return (key === "CLAUDECODE" ||
50
+ key.indexOf("CLAUDE_CODE_") === 0 ||
51
+ key === claudeSession_1.WATCH_ALLOW_IN_CLAUDE_ENV ||
52
+ key === "SN_USER" ||
53
+ key === "SN_INSTANCE" ||
54
+ key === "SN_PASSWORD");
55
+ }
56
+ describe("watchAllScopesCommand Claude Code guard", function () {
57
+ var savedEnv = {};
58
+ var exitSpy;
59
+ var sigintBefore = [];
60
+ beforeEach(function () {
61
+ jest.clearAllMocks();
62
+ savedEnv = {};
63
+ Object.keys(process.env).forEach(function (key) {
64
+ if (isManagedKey(key)) {
65
+ savedEnv[key] = process.env[key];
66
+ delete process.env[key];
67
+ }
68
+ });
69
+ process.env.SN_USER = "test-user";
70
+ process.env.SN_INSTANCE = "example.service-now.com";
71
+ process.env.SN_PASSWORD = "not-a-real-value";
72
+ sigintBefore = process.listeners("SIGINT").slice();
73
+ exitSpy = jest
74
+ .spyOn(process, "exit")
75
+ .mockImplementation(function (code) {
76
+ throw new ExitSignal(typeof code === "number" ? code : undefined);
77
+ });
78
+ });
79
+ afterEach(function () {
80
+ exitSpy.mockRestore();
81
+ process.listeners("SIGINT").forEach(function (listener) {
82
+ if (sigintBefore.indexOf(listener) === -1) {
83
+ process.removeListener("SIGINT", listener);
84
+ }
85
+ });
86
+ Object.keys(process.env).forEach(function (key) {
87
+ if (isManagedKey(key)) {
88
+ delete process.env[key];
89
+ }
90
+ });
91
+ Object.keys(savedEnv).forEach(function (key) {
92
+ if (savedEnv[key] !== undefined) {
93
+ process.env[key] = savedEnv[key];
94
+ }
95
+ });
96
+ });
97
+ it("exits 1 before starting the watcher when CLAUDECODE is set", async function () {
98
+ process.env.CLAUDECODE = "1";
99
+ await expect((0, allScopesCommands_1.watchAllScopesCommand)(ARGS)).rejects.toBeInstanceOf(ExitSignal);
100
+ expect(exitSpy).toHaveBeenCalledWith(1);
101
+ expect(mockStartWatching).not.toHaveBeenCalled();
102
+ expect(mockLogger.error).toHaveBeenCalledWith(claudeSession_1.WATCH_BLOCKED_IN_CLAUDE_ERROR);
103
+ expect(claudeSession_1.WATCH_BLOCKED_IN_CLAUDE_ERROR).toContain(claudeSession_1.WATCH_ALLOW_IN_CLAUDE_ENV + "=1");
104
+ });
105
+ it("starts (with the warning) when CLAUDECODE is set and the human override is 1", async function () {
106
+ process.env.CLAUDECODE = "1";
107
+ process.env[claudeSession_1.WATCH_ALLOW_IN_CLAUDE_ENV] = "1";
108
+ await (0, allScopesCommands_1.watchAllScopesCommand)(ARGS);
109
+ expect(exitSpy).not.toHaveBeenCalled();
110
+ expect(mockStartWatching).toHaveBeenCalledTimes(1);
111
+ expect(mockLogger.warn).toHaveBeenCalledWith(claudeSession_1.WATCH_HUMAN_ONLY_WARNING);
112
+ });
113
+ it("does not treat an override other than 1 as consent", async function () {
114
+ process.env.CLAUDECODE = "1";
115
+ process.env[claudeSession_1.WATCH_ALLOW_IN_CLAUDE_ENV] = "true";
116
+ await expect((0, allScopesCommands_1.watchAllScopesCommand)(ARGS)).rejects.toBeInstanceOf(ExitSignal);
117
+ expect(mockStartWatching).not.toHaveBeenCalled();
118
+ });
119
+ it("only warns (and starts) for a human shell with a CLAUDE_CODE_* config variable", async function () {
120
+ process.env.CLAUDE_CODE_USE_BEDROCK = "1";
121
+ await (0, allScopesCommands_1.watchAllScopesCommand)(ARGS);
122
+ expect(exitSpy).not.toHaveBeenCalled();
123
+ expect(mockStartWatching).toHaveBeenCalledTimes(1);
124
+ expect(mockLogger.warn).toHaveBeenCalledWith(claudeSession_1.WATCH_HUMAN_ONLY_WARNING);
125
+ });
126
+ it("starts silently with no Claude markers at all", async function () {
127
+ await (0, allScopesCommands_1.watchAllScopesCommand)(ARGS);
128
+ expect(exitSpy).not.toHaveBeenCalled();
129
+ expect(mockStartWatching).toHaveBeenCalledTimes(1);
130
+ expect(mockLogger.warn).not.toHaveBeenCalledWith(claudeSession_1.WATCH_HUMAN_ONLY_WARNING);
131
+ });
132
+ });
@@ -297,7 +297,7 @@ async function switchUpdateSetCommand(args) {
297
297
  try {
298
298
  const client = (0, snClient_1.defaultClient)();
299
299
  // Get update set to switch to
300
- const targetUpdateSet = await selectUpdateSet(args.name, args.scope);
300
+ const targetUpdateSet = await selectUpdateSet(args.name, args.scope, args.sysId);
301
301
  if (!targetUpdateSet) {
302
302
  throw new Error("No update set selected");
303
303
  }
@@ -633,8 +633,16 @@ async function getCurrentUpdateSetDetails(scope) {
633
633
  /**
634
634
  * Helper function to select an update set from a list
635
635
  */
636
- async function selectUpdateSet(nameFilter, scopeFilter) {
636
+ async function selectUpdateSet(nameFilter, scopeFilter, sysIdFilter) {
637
637
  let query = "state=in progress";
638
+ // An exact sys_id is unambiguous where a name may not be (two in-progress
639
+ // sets can share a name). Validate it before it reaches the encoded query.
640
+ if (sysIdFilter !== undefined) {
641
+ if (typeof sysIdFilter !== "string" || !/^[0-9a-f]{32}$/i.test(sysIdFilter)) {
642
+ Logger_1.logger.error(`Invalid --sysId "${String(sysIdFilter)}": expected a 32-character sys_id`);
643
+ return null;
644
+ }
645
+ }
638
646
  if (scopeFilter) {
639
647
  const client = (0, snClient_1.defaultClient)();
640
648
  const scopeResult = await (0, snClient_1.unwrapSNResponse)(client.getScopeId(scopeFilter));
@@ -644,6 +652,15 @@ async function selectUpdateSet(nameFilter, scopeFilter) {
644
652
  }
645
653
  query += `^application=${scopeResult[0].sys_id}`;
646
654
  }
655
+ if (sysIdFilter !== undefined) {
656
+ const byId = await getUpdateSets(`${query}^sys_id=${sysIdFilter}`);
657
+ if (byId.length === 0) {
658
+ Logger_1.logger.error(`No in-progress update set with sys_id ${sysIdFilter}` +
659
+ (scopeFilter ? ` in scope "${scopeFilter}"` : ""));
660
+ return null;
661
+ }
662
+ return byId[0];
663
+ }
647
664
  const updateSets = await getUpdateSets(query);
648
665
  if (updateSets.length === 0) {
649
666
  Logger_1.logger.info("No in-progress update sets found");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-core",
3
- "version": "0.0.125",
3
+ "version": "0.0.127",
4
4
  "description": "Next-gen file syncer",
5
5
  "license": "GPL-3.0",
6
6
  "main": "./dist/index.js",
@@ -1,7 +1,137 @@
1
1
  {
2
2
  "$schema": "dovetail-manifest/v1",
3
- "generated_at": "2026-10-08T05:20:00.147Z",
3
+ "generated_at": "2026-10-09T03:55:11.874Z",
4
4
  "events": [
5
+ {
6
+ "$schema": "dovetail-release-event/v1",
7
+ "event_id": "dovetail-core@0.0.126",
8
+ "package": "@tenonhq/dovetail-core",
9
+ "version": "0.0.126",
10
+ "prev_version": "0.0.125",
11
+ "published_at": "2026-10-09T03:55:11.868Z",
12
+ "git": {
13
+ "sha": "ca4fdead0bbd5c4f7ed3dd544ac3ae19a4037e42",
14
+ "tag": "dovetail-core@0.0.126"
15
+ },
16
+ "semver_bump": "patch",
17
+ "commits": [
18
+ {
19
+ "sha": "ca4fdead0",
20
+ "subject": "fix(core): dove --env accepts a bare instance name like dove-sn (#325)",
21
+ "pr": 325
22
+ },
23
+ {
24
+ "sha": "435341abf",
25
+ "subject": "chore(release): publish 2 packages [skip ci]"
26
+ },
27
+ {
28
+ "sha": "301dfbb75",
29
+ "subject": "fix: review fixes for the Oct 1-8 merges, re-land reverted #288 and #311 (#322)",
30
+ "pr": 322
31
+ },
32
+ {
33
+ "sha": "16fd9248d",
34
+ "subject": "chore(release): @tenonhq/dovetail-core@0.0.125 [skip ci]"
35
+ }
36
+ ],
37
+ "exports_delta": null,
38
+ "narrative": null
39
+ },
40
+ {
41
+ "$schema": "dovetail-release-event/v1",
42
+ "event_id": "dovetail-servicenow@0.0.52",
43
+ "package": "@tenonhq/dovetail-servicenow",
44
+ "version": "0.0.52",
45
+ "prev_version": "0.0.51",
46
+ "published_at": "2026-10-08T18:59:46.343Z",
47
+ "git": {
48
+ "sha": "b2fd9270f790cea252d16a6c57df65707fc97fb8",
49
+ "tag": "dovetail-servicenow@0.0.52"
50
+ },
51
+ "semver_bump": "patch",
52
+ "commits": [
53
+ {
54
+ "sha": "b2fd9270f",
55
+ "subject": "feat(servicenow): sync-ux-events — register now-ui.json actions as sys_ux_event on the macroponent (#324)",
56
+ "pr": 324
57
+ },
58
+ {
59
+ "sha": "301dfbb75",
60
+ "subject": "fix: review fixes for the Oct 1-8 merges, re-land reverted #288 and #311 (#322)",
61
+ "pr": 322
62
+ },
63
+ {
64
+ "sha": "c9f0b0528",
65
+ "subject": "chore(release): publish 2 packages [skip ci]"
66
+ }
67
+ ],
68
+ "exports_delta": null,
69
+ "narrative": null
70
+ },
71
+ {
72
+ "$schema": "dovetail-release-event/v1",
73
+ "event_id": "dovetail-mcp@0.0.50",
74
+ "package": "@tenonhq/dovetail-mcp",
75
+ "version": "0.0.50",
76
+ "prev_version": "0.0.49",
77
+ "published_at": "2026-10-08T18:59:46.343Z",
78
+ "git": {
79
+ "sha": "b2fd9270f790cea252d16a6c57df65707fc97fb8",
80
+ "tag": "dovetail-mcp@0.0.50"
81
+ },
82
+ "semver_bump": "patch",
83
+ "commits": [
84
+ {
85
+ "sha": "301dfbb75",
86
+ "subject": "fix: review fixes for the Oct 1-8 merges, re-land reverted #288 and #311 (#322)",
87
+ "pr": 322
88
+ },
89
+ {
90
+ "sha": "c9f0b0528",
91
+ "subject": "chore(release): publish 2 packages [skip ci]"
92
+ }
93
+ ],
94
+ "exports_delta": null,
95
+ "narrative": null
96
+ },
97
+ {
98
+ "$schema": "dovetail-release-event/v1",
99
+ "event_id": "dovetail-core@0.0.125",
100
+ "package": "@tenonhq/dovetail-core",
101
+ "version": "0.0.125",
102
+ "prev_version": "0.0.124",
103
+ "published_at": "2026-10-08T07:05:53.853Z",
104
+ "git": {
105
+ "sha": "d9cb3db0700edff976d54ac69497e4722452189f",
106
+ "tag": "dovetail-core@0.0.125"
107
+ },
108
+ "semver_bump": "patch",
109
+ "commits": [
110
+ {
111
+ "sha": "d9cb3db07",
112
+ "subject": "feat(core): dove pull <table> <sys_id> — per-record pull without a scope refresh (#321)",
113
+ "pr": 321
114
+ },
115
+ {
116
+ "sha": "c9f0b0528",
117
+ "subject": "chore(release): publish 2 packages [skip ci]"
118
+ },
119
+ {
120
+ "sha": "6579c6d00",
121
+ "subject": "chore(release): publish 2 packages [skip ci]"
122
+ },
123
+ {
124
+ "sha": "73c35c0c0",
125
+ "subject": "chore(release): publish 2 packages [skip ci]"
126
+ },
127
+ {
128
+ "sha": "c45a56a81",
129
+ "subject": "chore(release): publish 3 packages [skip ci]"
130
+ }
131
+ ],
132
+ "exports_delta": null,
133
+ "narrative": null
134
+ },
5
135
  {
6
136
  "$schema": "dovetail-release-event/v1",
7
137
  "event_id": "dovetail-servicenow@0.0.51",
@@ -1217,100 +1347,6 @@
1217
1347
  ],
1218
1348
  "exports_delta": null,
1219
1349
  "narrative": null
1220
- },
1221
- {
1222
- "$schema": "dovetail-release-event/v1",
1223
- "event_id": "dovetail-servicenow@0.0.38",
1224
- "package": "@tenonhq/dovetail-servicenow",
1225
- "version": "0.0.38",
1226
- "prev_version": "0.0.37",
1227
- "published_at": "2026-09-15T23:33:16.570Z",
1228
- "git": {
1229
- "sha": "925c0ca40cc6407fa78f28b25b82f8dc0fbd460f",
1230
- "tag": "dovetail-servicenow@0.0.38"
1231
- },
1232
- "semver_bump": "patch",
1233
- "commits": [
1234
- {
1235
- "sha": "925c0ca40",
1236
- "subject": "feat(servicenow): headless update-set / app export with mandatory secret stripping (#276)",
1237
- "pr": 276
1238
- },
1239
- {
1240
- "sha": "83df94645",
1241
- "subject": "chore(release): publish 2 packages [skip ci]"
1242
- }
1243
- ],
1244
- "exports_delta": null,
1245
- "narrative": null
1246
- },
1247
- {
1248
- "$schema": "dovetail-release-event/v1",
1249
- "event_id": "dovetail-mcp@0.0.35",
1250
- "package": "@tenonhq/dovetail-mcp",
1251
- "version": "0.0.35",
1252
- "prev_version": "0.0.34",
1253
- "published_at": "2026-09-15T23:33:16.570Z",
1254
- "git": {
1255
- "sha": "925c0ca40cc6407fa78f28b25b82f8dc0fbd460f",
1256
- "tag": "dovetail-mcp@0.0.35"
1257
- },
1258
- "semver_bump": "patch",
1259
- "commits": [
1260
- {
1261
- "sha": "83df94645",
1262
- "subject": "chore(release): publish 2 packages [skip ci]"
1263
- }
1264
- ],
1265
- "exports_delta": null,
1266
- "narrative": null
1267
- },
1268
- {
1269
- "$schema": "dovetail-release-event/v1",
1270
- "event_id": "dovetail-servicenow@0.0.37",
1271
- "package": "@tenonhq/dovetail-servicenow",
1272
- "version": "0.0.37",
1273
- "prev_version": "0.0.36",
1274
- "published_at": "2026-09-15T22:43:57.664Z",
1275
- "git": {
1276
- "sha": "420fbbb20842c3369ed85e9b3325fd1ead989540",
1277
- "tag": "dovetail-servicenow@0.0.37"
1278
- },
1279
- "semver_bump": "patch",
1280
- "commits": [
1281
- {
1282
- "sha": "420fbbb20",
1283
- "subject": "feat(servicenow): add-index — create a unique index on a scoped table via the dictionary unique flag (#275)",
1284
- "pr": 275
1285
- },
1286
- {
1287
- "sha": "4576c072f",
1288
- "subject": "chore(release): publish 15 packages [skip ci]"
1289
- }
1290
- ],
1291
- "exports_delta": null,
1292
- "narrative": null
1293
- },
1294
- {
1295
- "$schema": "dovetail-release-event/v1",
1296
- "event_id": "dovetail-mcp@0.0.34",
1297
- "package": "@tenonhq/dovetail-mcp",
1298
- "version": "0.0.34",
1299
- "prev_version": "0.0.33",
1300
- "published_at": "2026-09-15T22:43:57.664Z",
1301
- "git": {
1302
- "sha": "420fbbb20842c3369ed85e9b3325fd1ead989540",
1303
- "tag": "dovetail-mcp@0.0.34"
1304
- },
1305
- "semver_bump": "patch",
1306
- "commits": [
1307
- {
1308
- "sha": "4576c072f",
1309
- "subject": "chore(release): publish 15 packages [skip ci]"
1310
- }
1311
- ],
1312
- "exports_delta": null,
1313
- "narrative": null
1314
1350
  }
1315
1351
  ]
1316
1352
  }
@@ -56,7 +56,7 @@ SN_INSTANCE=your-instance.service-now.com
56
56
  - Instance should NOT have `https://` prefix or trailing slash
57
57
  - Optional: `DASHBOARD_PORT=3456`
58
58
  - **Never commit `.env` to git**
59
- - **Multiple instances?** Keep one file per instance (`.env.dev`, `.env.prod`) and pass `--env <path>` (alias `-e`) on any command: `npx dove push --env .env.prod`. `dove login --env .env.prod` writes credentials to that file too. Git-ignore the whole pattern (`.env*`).
59
+ - **Multiple instances?** Keep one file per instance (`.env.dev`, `.env.prod`) and pass `--env <name|path>` (alias `-e`) on any command: `npx dove push --env prod` (bare name → `.env.prod` in the cwd) or `npx dove push --env .env.prod`. `dove login --env prod` writes credentials to that file too. Git-ignore the whole pattern (`.env*`).
60
60
 
61
61
  #### Set up `.gitignore`
62
62
  ```
@@ -49,7 +49,7 @@ Ask the user which symptom they are experiencing (if not already clear):
49
49
  - Instance should NOT have `https://` prefix or trailing slash
50
50
  - Credentials must have admin or developer role
51
51
 
52
- 2. **Test connection:** `npx dove status` (add `--env <path>` to test a specific credential file, e.g. `npx dove status --env .env.prod`)
52
+ 2. **Test connection:** `npx dove status` (add `--env <name|path>` to test a specific credential file, e.g. `npx dove status --env prod` for `.env.prod` in the cwd)
53
53
 
54
54
  3. **Check the Dovetail server scoped app** is installed on the instance. Without it, API endpoints will 404.
55
55