@nessielabs/daemon 0.2.0 → 0.2.2

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/README.md CHANGED
@@ -11,10 +11,11 @@ nessie-daemon setup
11
11
 
12
12
  `setup` authenticates the device, detects local Codex and Claude Code history directories, lets you choose which sources to ingest, and can install the user `systemd` service.
13
13
 
14
- Use non-production endpoints with:
14
+ For non-interactive headless setup, pass a Nessie API key:
15
15
 
16
16
  ```bash
17
- nessie-daemon setup --api-url https://sync.example --auth-api-url https://auth.example
17
+ nessie-daemon setup --api-key "$NESSIE_API_KEY"
18
+ nessie-daemon start
18
19
  ```
19
20
 
20
21
  ## Commands
@@ -1,10 +1,11 @@
1
1
  import { setTimeout as sleep } from "node:timers/promises";
2
2
  import { postJson } from "../api/http.js";
3
+ import { authApiUrl } from "../config/endpoints.js";
3
4
  export async function runDeviceAuth(input) {
4
5
  const log = input.log ?? console;
5
6
  const fetchImpl = input.fetchImpl ?? fetch;
6
7
  const sleepMs = input.sleepMs ?? sleep;
7
- const start = await postJson(input.authApiUrl, "/auth/device/start", {
8
+ const start = await postJson(authApiUrl, "/auth/device/start", {
8
9
  device_name: input.deviceName,
9
10
  platform: "linux",
10
11
  }, { fetchImpl });
@@ -13,7 +14,7 @@ export async function runDeviceAuth(input) {
13
14
  const expiresAt = Date.now() + Math.max(60, start.expires_in ?? 600) * 1000;
14
15
  while (Date.now() < expiresAt) {
15
16
  await sleepMs(intervalMs);
16
- const poll = await postJson(input.authApiUrl, "/auth/device/poll", {
17
+ const poll = await postJson(authApiUrl, "/auth/device/poll", {
17
18
  device_code: start.device_code,
18
19
  }, { fetchImpl });
19
20
  if (poll.status === "pending")
@@ -1,4 +1,5 @@
1
1
  import { ApiError, postJson } from "../api/http.js";
2
+ import { authApiUrl } from "../config/endpoints.js";
2
3
  export class AuthRefreshError extends Error {
3
4
  }
4
5
  export async function refreshAccessToken(config, options = {}) {
@@ -6,7 +7,7 @@ export async function refreshAccessToken(config, options = {}) {
6
7
  throw new AuthRefreshError("Access token expired and no refresh token is configured.");
7
8
  }
8
9
  try {
9
- const response = await postJson(config.authApiUrl, "/auth/refresh", {
10
+ const response = await postJson(authApiUrl, "/auth/refresh", {
10
11
  refresh_token: config.refreshToken,
11
12
  }, { fetchImpl: options.fetchImpl });
12
13
  if (!response.access_token) {
package/dist/cli.js CHANGED
@@ -7,12 +7,10 @@ const program = new Command();
7
7
  program
8
8
  .name("nessie-daemon")
9
9
  .description("Headless Nessie ingestion daemon for Linux agent machines.")
10
- .version("0.2.0");
10
+ .version("0.2.2");
11
11
  program
12
12
  .command("setup")
13
13
  .description("Authenticate, select local sources, and optionally install the user systemd service.")
14
- .option("--api-url <url>", "Nessie sync API URL")
15
- .option("--auth-api-url <url>", "Nessie auth API URL")
16
14
  .option("--api-key <key>", "Use a Nessie API key for non-interactive headless setup.")
17
15
  .action(runSetup);
18
16
  program
@@ -2,25 +2,21 @@ import { checkbox, confirm } from "@inquirer/prompts";
2
2
  import { hostname } from "node:os";
3
3
  import { runDeviceAuth } from "../auth/deviceFlow.js";
4
4
  import { getDaemonPaths } from "../config/paths.js";
5
- import { createConfig, createInitialState, defaultApiUrl, defaultAuthApiUrl, writeConfig, writeState } from "../config/store.js";
5
+ import { createConfig, createInitialState, writeConfig, writeState } from "../config/store.js";
6
6
  import { installUserService } from "../service/systemd.js";
7
7
  import { detectSources } from "../sources/detect.js";
8
8
  export async function runSetup(options, depsOverride = {}) {
9
9
  const deps = resolveSetupDeps(depsOverride);
10
- const apiUrl = options.apiUrl ?? defaultApiUrl;
11
- const authApiUrl = options.authApiUrl ?? defaultAuthApiUrl;
12
10
  const deviceName = hostname();
13
11
  if (options.apiKey !== undefined) {
14
12
  await runAPIKeySetup({
15
13
  apiKey: options.apiKey,
16
- apiUrl,
17
- authApiUrl,
18
14
  deviceName,
19
15
  deps,
20
16
  });
21
17
  return;
22
18
  }
23
- const auth = await runDeviceAuth({ authApiUrl, deviceName });
19
+ const auth = await runDeviceAuth({ deviceName });
24
20
  const detectedSources = await deps.detectSources();
25
21
  if (detectedSources.length === 0) {
26
22
  throw new Error("No supported Codex or Claude Code history directories were found.");
@@ -39,8 +35,6 @@ export async function runSetup(options, depsOverride = {}) {
39
35
  refreshToken: auth.refreshToken,
40
36
  deviceId: auth.deviceId,
41
37
  deviceName,
42
- apiUrl,
43
- authApiUrl,
44
38
  selectedSources,
45
39
  });
46
40
  await deps.writeConfig(config, deps.paths);
@@ -62,8 +56,6 @@ async function runAPIKeySetup(input) {
62
56
  const config = createConfig({
63
57
  apiKey,
64
58
  deviceName: input.deviceName,
65
- apiUrl: input.apiUrl,
66
- authApiUrl: input.authApiUrl,
67
59
  selectedSources: detectedSources,
68
60
  });
69
61
  await input.deps.writeConfig(config, input.deps.paths);
@@ -0,0 +1,2 @@
1
+ export const syncApiUrl = "https://nessie-codebase-843813578359.us-west1.run.app";
2
+ export const authApiUrl = "https://nessie-notes-go-843813578359.us-west1.run.app";
@@ -3,13 +3,9 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { hostname } from "node:os";
4
4
  import { dirname } from "node:path";
5
5
  import { getDaemonPaths } from "./paths.js";
6
- export const defaultApiUrl = "https://nessie-codebase-843813578359.us-west1.run.app";
7
- export const defaultAuthApiUrl = "https://nessie-notes-go-843813578359.us-west1.run.app";
8
6
  export function createConfig(input) {
9
7
  const deviceId = input.deviceId ?? randomUUID();
10
8
  return {
11
- apiUrl: input.apiUrl ?? defaultApiUrl,
12
- authApiUrl: input.authApiUrl ?? defaultAuthApiUrl,
13
9
  deviceId,
14
10
  deviceName: input.deviceName ?? hostname(),
15
11
  accessToken: input.accessToken,
@@ -26,6 +22,7 @@ export function createInitialState() {
26
22
  return {
27
23
  files: {},
28
24
  pushedSourceRootIds: [],
25
+ pushedSessions: {},
29
26
  lastSyncAt: null,
30
27
  };
31
28
  }
@@ -42,6 +39,7 @@ export async function readState(paths = getDaemonPaths()) {
42
39
  return {
43
40
  files: parsed.files ?? {},
44
41
  pushedSourceRootIds: parsed.pushedSourceRootIds ?? [],
42
+ pushedSessions: parsed.pushedSessions ?? {},
45
43
  lastSyncAt: parsed.lastSyncAt ?? null,
46
44
  };
47
45
  }
@@ -1,5 +1,6 @@
1
1
  import { ApiError, postJson } from "../api/http.js";
2
2
  import { refreshAccessToken } from "../auth/tokens.js";
3
+ import { syncApiUrl } from "../config/endpoints.js";
3
4
  import { writeConfig } from "../config/store.js";
4
5
  export async function runPreflightV3(config, options = {}) {
5
6
  return postJsonWithRefresh(config, "/sync/v3/preflight", {
@@ -24,7 +25,7 @@ export async function pushSyncPayload(config, payload, options = {}) {
24
25
  async function postJsonWithRefresh(config, path, body, options) {
25
26
  const token = authToken(config);
26
27
  try {
27
- await postJson(config.apiUrl, path, body, { token, fetchImpl: options.fetchImpl });
28
+ await postJson(syncApiUrl, path, body, { token, fetchImpl: options.fetchImpl });
28
29
  return config;
29
30
  }
30
31
  catch (error) {
@@ -35,7 +36,7 @@ async function postJsonWithRefresh(config, path, body, options) {
35
36
  }
36
37
  const refreshed = await refreshAccessToken(config, { fetchImpl: options.fetchImpl });
37
38
  await (options.persistConfig ?? writeConfig)(refreshed);
38
- await postJson(refreshed.apiUrl, path, body, { token: authToken(refreshed), fetchImpl: options.fetchImpl });
39
+ await postJson(syncApiUrl, path, body, { token: authToken(refreshed), fetchImpl: options.fetchImpl });
39
40
  return refreshed;
40
41
  }
41
42
  function authToken(config) {
package/dist/sync/loop.js CHANGED
@@ -13,14 +13,19 @@ export async function runSyncOnce() {
13
13
  const edges = [];
14
14
  const previouslyPushedRoots = new Set(state.pushedSourceRootIds);
15
15
  const pushedSourceRootIds = new Set(previouslyPushedRoots);
16
+ const pushedSessions = { ...state.pushedSessions };
16
17
  for (const source of config.selectedSources) {
17
18
  const sessions = scan.sessionsBySourceId.get(source.localId) ?? [];
18
19
  const includeRoot = !previouslyPushedRoots.has(source.localId);
19
- const mapped = mapSessionsToPushItems(source, sessions, { includeRoot });
20
+ const mapped = mapSessionsToPushItems(source, sessions, {
21
+ includeRoot,
22
+ pushedSessions: state.pushedSessions,
23
+ });
20
24
  nodes.push(...mapped.nodes);
21
25
  edges.push(...mapped.edges);
22
26
  if (includeRoot)
23
27
  pushedSourceRootIds.add(source.localId);
28
+ Object.assign(pushedSessions, mapped.pushedSessions);
24
29
  }
25
30
  if (nodes.length > 0 || edges.length > 0) {
26
31
  config = await pushSyncPayload(config, { nodes, edges });
@@ -28,6 +33,7 @@ export async function runSyncOnce() {
28
33
  await writeState({
29
34
  ...scan.nextState,
30
35
  pushedSourceRootIds: [...pushedSourceRootIds].sort(),
36
+ pushedSessions,
31
37
  lastSyncAt: new Date().toISOString(),
32
38
  });
33
39
  return { nodes: nodes.length, edges: edges.length };
package/dist/sync/map.js CHANGED
@@ -1,28 +1,42 @@
1
1
  import { redactSecrets } from "../redact/secrets.js";
2
2
  import { contentHash, stableUuidFromString } from "./ids.js";
3
3
  export function mapSessionsToPushItems(source, sessions, options = { includeRoot: true }) {
4
+ const pushedSessions = options.pushedSessions ?? {};
5
+ const nextPushedSessions = {};
4
6
  const nodes = options.includeRoot ? [mapRootNode(source)] : [];
5
7
  const edges = [];
6
8
  for (const session of sessions) {
7
9
  const conversationId = stableUuidFromString(`${source.localId}:session:${session.id}`);
8
- nodes.push({
9
- node: {
10
- id: conversationId,
11
- integrationId: source.localId,
12
- sourceId: session.sourceId,
13
- name: redactSecrets(session.title),
14
- kind: session.sourceKind === "codex" ? "codex_chat" : "claude_code_chat",
15
- originalCreatedAt: session.createdAt,
16
- originalUpdatedAt: session.updatedAt,
17
- },
18
- baseCloudUpdatedAt: null,
19
- slices: [],
20
- aiChatMessage: null,
21
- nessieChatMessage: null,
22
- });
23
- edges.push(mapEdge(source.localId, conversationId, "source_of", 0));
10
+ const stateKey = sessionStateKey(source, session);
11
+ const sessionCursor = pushedSessions[stateKey] ?? {
12
+ conversationPushed: false,
13
+ pushedMessageCount: 0,
14
+ };
15
+ if (!sessionCursor.conversationPushed) {
16
+ nodes.push({
17
+ node: {
18
+ id: conversationId,
19
+ integrationId: source.localId,
20
+ sourceId: session.sourceId,
21
+ name: redactSecrets(session.title),
22
+ kind: session.sourceKind === "codex" ? "codex_chat" : "claude_code_chat",
23
+ originalCreatedAt: session.createdAt,
24
+ originalUpdatedAt: session.updatedAt,
25
+ },
26
+ baseCloudUpdatedAt: null,
27
+ slices: [],
28
+ aiChatMessage: null,
29
+ nessieChatMessage: null,
30
+ });
31
+ edges.push(mapEdge(source.localId, conversationId, "source_of", 0));
32
+ }
24
33
  const pushableMessages = session.messages.filter((message) => message.role !== "system");
25
- pushableMessages.forEach((message, index) => {
34
+ nextPushedSessions[stateKey] = {
35
+ conversationPushed: true,
36
+ pushedMessageCount: pushableMessages.length,
37
+ };
38
+ pushableMessages.slice(sessionCursor.pushedMessageCount).forEach((message, offset) => {
39
+ const index = sessionCursor.pushedMessageCount + offset;
26
40
  const messageId = stableUuidFromString(`${conversationId}:message:${index}`);
27
41
  const content = redactSecrets(message.content);
28
42
  const timestamp = message.timestamp ?? session.updatedAt;
@@ -54,7 +68,10 @@ export function mapSessionsToPushItems(source, sessions, options = { includeRoot
54
68
  edges.push(mapEdge(conversationId, messageId, "contains", index));
55
69
  });
56
70
  }
57
- return { nodes, edges };
71
+ return { nodes, edges, pushedSessions: nextPushedSessions };
72
+ }
73
+ export function sessionStateKey(source, session) {
74
+ return `${source.localId}:session:${session.id}`;
58
75
  }
59
76
  function mapRootNode(source) {
60
77
  const now = new Date().toISOString();
@@ -75,9 +92,10 @@ function mapRootNode(source) {
75
92
  };
76
93
  }
77
94
  function mapEdge(fromNodeId, toNodeId, type, sortIndex) {
95
+ const id = stableUuidFromString(`${fromNodeId}:${type}:${toNodeId}`);
78
96
  return {
79
97
  edge: {
80
- id: stableUuidFromString(`${fromNodeId}:${type}:${toNodeId}`),
98
+ id,
81
99
  fromNodeId,
82
100
  toNodeId,
83
101
  type,
package/dist/sync/scan.js CHANGED
@@ -6,6 +6,7 @@ export async function scanSelectedSources(sources, state) {
6
6
  const nextState = {
7
7
  files: { ...state.files },
8
8
  pushedSourceRootIds: [...state.pushedSourceRootIds],
9
+ pushedSessions: { ...state.pushedSessions },
9
10
  lastSyncAt: state.lastSyncAt,
10
11
  };
11
12
  const sessionsBySourceId = new Map();
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@nessielabs/daemon",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Headless Linux ingestion daemon for Nessie agent traces.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
7
7
  "bin": {
8
- "nessie-daemon": "./dist/cli.js"
8
+ "nessie-daemon": "dist/cli.js"
9
9
  },
10
10
  "files": [
11
11
  "dist/**/*.js",
@@ -18,7 +18,7 @@
18
18
  "node": ">=20"
19
19
  },
20
20
  "scripts": {
21
- "build": "rm -rf dist && tsc -p tsconfig.json",
21
+ "build": "rm -rf dist && tsc -p tsconfig.json && chmod +x dist/cli.js",
22
22
  "check": "tsc -p tsconfig.json --noEmit",
23
23
  "dev": "tsx src/cli.ts",
24
24
  "test": "vitest run"