@nessielabs/daemon 0.3.2 → 0.5.26

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.
Files changed (49) hide show
  1. package/README.md +8 -19
  2. package/dist/cli.js +46 -18
  3. package/dist/commands/auth.js +116 -0
  4. package/dist/commands/integrations.js +84 -0
  5. package/dist/commands/login.js +145 -0
  6. package/dist/commands/run.js +21 -6
  7. package/dist/commands/service.js +18 -36
  8. package/dist/commands/sharing.js +399 -0
  9. package/dist/commands/status.js +67 -0
  10. package/dist/commands/teams.js +64 -0
  11. package/dist/nera/client.js +156 -0
  12. package/dist/nera/process.js +78 -0
  13. package/dist/nera/runtimeFile.js +25 -0
  14. package/dist/platform/binary.js +21 -0
  15. package/dist/platform/openBrowser.js +14 -0
  16. package/dist/platform/paths.js +13 -0
  17. package/dist/platform/table.js +85 -0
  18. package/package.json +9 -3
  19. package/vendor/nera/darwin-arm64/nera +0 -0
  20. package/vendor/nera/darwin-x64/nera +0 -0
  21. package/vendor/nera/linux-arm64/nera +0 -0
  22. package/vendor/nera/linux-x64/nera +0 -0
  23. package/vendor/nera/win32-arm64/nera.exe +0 -0
  24. package/vendor/nera/win32-x64/nera.exe +0 -0
  25. package/vendor/proto/nessie.edge.v1.proto +475 -0
  26. package/dist/api/http.js +0 -25
  27. package/dist/auth/deviceFlow.js +0 -36
  28. package/dist/auth/tokens.js +0 -30
  29. package/dist/commands/setup.js +0 -78
  30. package/dist/config/endpoints.js +0 -2
  31. package/dist/config/paths.js +0 -19
  32. package/dist/config/store.js +0 -63
  33. package/dist/git/repoResolver.js +0 -215
  34. package/dist/parser/claudeCode.js +0 -84
  35. package/dist/parser/codex.js +0 -86
  36. package/dist/parser/jsonl.js +0 -24
  37. package/dist/parser/types.js +0 -9
  38. package/dist/redact/secrets.js +0 -60
  39. package/dist/service/process.js +0 -134
  40. package/dist/service/systemd.js +0 -38
  41. package/dist/sources/detect.js +0 -43
  42. package/dist/sync/batch.js +0 -50
  43. package/dist/sync/client.js +0 -47
  44. package/dist/sync/ids.js +0 -11
  45. package/dist/sync/loop.js +0 -76
  46. package/dist/sync/map.js +0 -111
  47. package/dist/sync/scan.js +0 -60
  48. package/dist/sync/slices.js +0 -74
  49. package/dist/sync/types.js +0 -1
package/README.md CHANGED
@@ -1,15 +1,15 @@
1
1
  # Nessie Daemon
2
2
 
3
- Headless Linux ingestion daemon for Nessie agent traces.
3
+ Headless Nessie command line interface.
4
4
 
5
5
  ## Install
6
6
 
7
7
  ```bash
8
8
  npm install -g @nessielabs/daemon
9
- nessie-daemon setup
9
+ nessie-daemon login
10
10
  ```
11
11
 
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.
12
+ `login` starts Nessie, signs in through the browser, detects local agent history directories, lets you configure data integrations, and starts ingestion plus cloud sync.
13
13
 
14
14
  For non-interactive headless setup, pass a Nessie API key:
15
15
 
@@ -21,6 +21,7 @@ nessie-daemon start
21
21
  ## Commands
22
22
 
23
23
  ```bash
24
+ nessie-daemon login
24
25
  nessie-daemon setup
25
26
  nessie-daemon run --once
26
27
  nessie-daemon start
@@ -28,24 +29,12 @@ nessie-daemon stop
28
29
  nessie-daemon status
29
30
  ```
30
31
 
31
- The daemon registers selected local resources through sync preflight and pushes local agent traces. It does not pull cloud data.
32
-
33
- Config is stored at `~/.config/nessie-daemon/config.json`; sync state is stored at `~/.local/state/nessie-daemon/state.json`.
34
-
35
- Re-running `setup` rewrites the source selection and resets sync cursors, which can trigger a full re-scan and idempotent re-push of local history.
32
+ The package installs only the `nessie-daemon` binary to avoid colliding with the Nessie Mac CLI.
36
33
 
37
- The first implementation uses file `mtime` and `size` cursors. When a JSONL file changes, the daemon reparses that file and relies on stable node IDs for idempotent pushes.
34
+ The npm daemon package embeds the Nessie runtime for the current platform. Node is a one-shot CLI wrapper: commands start or configure Nessie and then exit, while the background service remains running.
38
35
 
39
36
  ## systemd
40
37
 
41
- `setup` can install a user service at:
38
+ `nessie-daemon start` starts Nessie, enables ingestion and cloud sync timers, and exits. `nessie-daemon run` does the same for backwards compatibility. Logs are written under the user-local daemon state directory.
42
39
 
43
- ```bash
44
- ~/.config/systemd/user/nessie-daemon.service
45
- ```
46
-
47
- On long-running headless VMs, enable user services to survive logout:
48
-
49
- ```bash
50
- sudo loginctl enable-linger "$USER"
51
- ```
40
+ Existing user systemd units that invoke `nessie-daemon run` will only start or probe Nessie and then exit. They are no longer required and should be disabled or removed. This package no longer writes new systemd unit files itself.
package/dist/cli.js CHANGED
@@ -1,33 +1,61 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
2
3
  import { Command } from "commander";
3
- import { runDaemon } from "./commands/run.js";
4
- import { runServiceCommand } from "./commands/service.js";
5
- import { runSetup } from "./commands/setup.js";
4
+ import { registerAuthCommands } from "./commands/auth.js";
5
+ import { registerIntegrationCommands } from "./commands/integrations.js";
6
+ import { login } from "./commands/login.js";
7
+ import { run } from "./commands/run.js";
8
+ import { service } from "./commands/service.js";
9
+ import { registerSharingCommands } from "./commands/sharing.js";
10
+ import { status } from "./commands/status.js";
11
+ import { registerTeamCommands } from "./commands/teams.js";
12
+ const require = createRequire(import.meta.url);
13
+ const { version } = require("../package.json");
6
14
  const program = new Command();
7
15
  program
8
16
  .name("nessie-daemon")
9
- .description("Headless Nessie ingestion daemon for Linux agent machines.")
10
- .version("0.3.1");
17
+ .description("Nessie command line interface.")
18
+ .version(version);
11
19
  program
12
- .command("setup")
13
- .description("Authenticate, select local sources, and optionally install the user systemd service.")
20
+ .command("login")
21
+ .alias("setup")
22
+ .description("Sign in, configure data integrations, and start Nessie.")
14
23
  .option("--api-key <key>", "Use a Nessie API key for non-interactive headless setup.")
15
- .action(runSetup);
24
+ .option("--no-open", "Print the browser sign-in URL instead of opening it.")
25
+ .option("--no-background", "Do not start the background service after setup.")
26
+ .action(login);
16
27
  program
17
28
  .command("run")
18
- .description("Run the daemon sync loop.")
19
- .option("--once", "Run one sync pass and exit.")
20
- .option("--interval-seconds <seconds>", "Seconds between sync passes.", "60")
21
- .action(runDaemon);
29
+ .description("Start Nessie and enable background processing.")
30
+ .option("--once", "Trigger one ingestion and cloud-sync cycle, then exit.")
31
+ .option("--interval-seconds <seconds>", "Ignored compatibility option from the old daemon.")
32
+ .action(run);
33
+ registerAuthCommands(program);
34
+ registerIntegrationCommands(program);
35
+ registerSharingCommands(program);
36
+ registerTeamCommands(program);
22
37
  program
23
38
  .command("start")
24
- .description("Start the daemon.")
25
- .option("--background", "Start as a detached background process without systemd.")
26
- .action((options) => runServiceCommand("start", options));
39
+ .description("Start Nessie in the background.")
40
+ .option("--background", "Compatibility flag; background mode is the default.")
41
+ .action(async () => printIfPresent(await service("start")));
27
42
  for (const commandName of ["stop", "status"]) {
28
43
  program
29
44
  .command(commandName)
30
- .description(`${commandName[0]?.toUpperCase()}${commandName.slice(1)} the daemon.`)
31
- .action(() => runServiceCommand(commandName));
45
+ .description(`${commandName[0]?.toUpperCase()}${commandName.slice(1)} Nessie.`)
46
+ .action(async () => commandName === "status" ? status() : printIfPresent(await service(commandName)));
32
47
  }
33
- await program.parseAsync();
48
+ function printIfPresent(message) {
49
+ if (message.length > 0)
50
+ console.log(message);
51
+ }
52
+ // Node 26 can warn about unsettled top-level await while command actions are
53
+ // waiting on the runtime; keep the process alive until commander settles.
54
+ const keepAlive = setTimeout(() => undefined, 2 ** 31 - 1);
55
+ program
56
+ .parseAsync()
57
+ .catch((error) => {
58
+ console.error(error instanceof Error ? error.message : String(error));
59
+ process.exitCode = 1;
60
+ })
61
+ .finally(() => clearTimeout(keepAlive));
@@ -0,0 +1,116 @@
1
+ import { hostname } from "node:os";
2
+ import { AuthStatus, beginSignIn, getAuthState, setApiKey, signOut, startDevicePairing, switchAccount } from "../nera/client.js";
3
+ import { ensureRuntime } from "../nera/process.js";
4
+ import { openBrowser } from "../platform/openBrowser.js";
5
+ import { renderTabTable } from "../platform/table.js";
6
+ import { billingLines } from "./status.js";
7
+ export function registerAuthCommands(program) {
8
+ const auth = program.command("auth").description("Manage Nessie sign-in.");
9
+ auth.command("status").description("Show signed-in accounts.").action(authStatus);
10
+ auth
11
+ .command("login")
12
+ .description("Sign in to Nessie.")
13
+ .option("--api-key <key>", "Use a Nessie API key for non-interactive headless setup.")
14
+ .option("--pair", "Use device pairing instead of browser sign-in.")
15
+ .option("--no-open", "Print the browser sign-in URL instead of opening it.")
16
+ .action(authLogin);
17
+ auth.command("switch <user-id>").description("Switch active account.").action(authSwitch);
18
+ auth.command("logout [user-id]").description("Sign out the active account or a specific account.").action(authLogout);
19
+ }
20
+ async function authStatus() {
21
+ const runtime = await ensureRuntime({ detached: true });
22
+ const auth = await getAuthState(runtime);
23
+ const accounts = auth.accounts ?? [];
24
+ if (accounts.length === 0) {
25
+ console.log("Not signed in.");
26
+ return;
27
+ }
28
+ const rows = [
29
+ ["ACTIVE", "EMAIL", "ID"],
30
+ ...accounts.map((account) => [account.id === auth.user?.id ? "*" : "", account.email, account.id]),
31
+ ];
32
+ const colors = [
33
+ ["bold", "bold", "bold"],
34
+ ...accounts.map((account) => [account.id === auth.user?.id ? "green" : "dim", undefined, "dim"]),
35
+ ];
36
+ process.stdout.write(renderTabTable(rows, colors));
37
+ for (const line of billingLines(auth.billing))
38
+ console.log(line);
39
+ }
40
+ async function authLogin(options) {
41
+ if (options.apiKey !== undefined && options.pair === true)
42
+ throw new Error("--api-key and --pair cannot be used together.");
43
+ if (options.apiKey !== undefined && !options.apiKey.trim())
44
+ throw new Error("--api-key cannot be empty.");
45
+ const runtime = await ensureRuntime({ detached: true });
46
+ if (options.apiKey !== undefined) {
47
+ const apiKey = options.apiKey.trim();
48
+ await setApiKey(runtime, apiKey);
49
+ }
50
+ else if (options.pair === true) {
51
+ await authPair(runtime);
52
+ return;
53
+ }
54
+ else {
55
+ const beforeAuth = await getAuthState(runtime);
56
+ if (beforeAuth.status !== AuthStatus.SIGNED_IN) {
57
+ const { url } = await beginSignIn(runtime);
58
+ if (options.open === false)
59
+ console.log(`Open this URL to sign in: ${url}`);
60
+ else {
61
+ await openBrowser(url);
62
+ console.log("Opened browser sign-in.");
63
+ }
64
+ await waitForSignIn(runtime, beforeAuth);
65
+ }
66
+ }
67
+ let auth = await getAuthState(runtime);
68
+ if (auth.user?.email)
69
+ console.log(`Signed in as ${auth.user.email}.`);
70
+ // The billing refresh is fired async on install; give it a bounded
71
+ // moment to land so login can report the plan. Silence is fine if the
72
+ // fetch is slow - `nessie-daemon status` shows it later.
73
+ const deadline = Date.now() + 3000;
74
+ while (!auth.billing?.tier && Date.now() < deadline) {
75
+ await new Promise((resolve) => setTimeout(resolve, 250));
76
+ auth = await getAuthState(runtime);
77
+ }
78
+ for (const line of billingLines(auth.billing))
79
+ console.log(line);
80
+ }
81
+ async function authPair(runtime) {
82
+ const pairing = await startDevicePairing(runtime, hostname());
83
+ console.log(`Enter code ${pairing.userCode} in Nessie.`);
84
+ if (pairing.verificationHint)
85
+ console.log(pairing.verificationHint);
86
+ if (pairing.expiresAt)
87
+ console.log(`Expires at ${pairing.expiresAt}.`);
88
+ }
89
+ async function authSwitch(userId) {
90
+ const runtime = await ensureRuntime({ detached: true });
91
+ const auth = await switchAccount(runtime, userId);
92
+ if (auth.user?.email)
93
+ console.log(`Switched to ${auth.user.email}.`);
94
+ }
95
+ async function authLogout(userId) {
96
+ const runtime = await ensureRuntime({ detached: true });
97
+ const auth = await signOut(runtime, userId);
98
+ if (auth.user?.email)
99
+ console.log(`Signed in as ${auth.user.email}.`);
100
+ else
101
+ console.log("Signed out.");
102
+ }
103
+ async function waitForSignIn(runtime, beforeAuth) {
104
+ const beforeAccountIds = new Set((beforeAuth.accounts ?? []).map((account) => account.id));
105
+ const alreadySignedIn = beforeAuth.status === AuthStatus.SIGNED_IN || beforeAuth.status === AuthStatus.REAUTH_REQUIRED;
106
+ const deadline = Date.now() + 5 * 60 * 1000;
107
+ while (Date.now() < deadline) {
108
+ const state = await getAuthState(runtime);
109
+ const signedIn = state.status === AuthStatus.SIGNED_IN || state.status === AuthStatus.REAUTH_REQUIRED;
110
+ const hasNewAccount = (state.accounts ?? []).some((account) => !beforeAccountIds.has(account.id));
111
+ if (signedIn && (!alreadySignedIn || hasNewAccount || state.user?.id !== beforeAuth.user?.id || state.status !== beforeAuth.status))
112
+ return;
113
+ await new Promise((resolve) => setTimeout(resolve, 1000));
114
+ }
115
+ throw new Error("Timed out waiting for browser sign-in.");
116
+ }
@@ -0,0 +1,84 @@
1
+ import { addSource, configureSource, discoverSources, listSources, removeSource, startIngestion, stopIngestion, triggerIngestion } from "../nera/client.js";
2
+ import { ensureRuntime } from "../nera/process.js";
3
+ import { renderTabTable } from "../platform/table.js";
4
+ export function registerIntegrationCommands(program) {
5
+ const integrations = program.command("integrations").description("Manage local data integrations.");
6
+ integrations.command("list").alias("ls").description("List configured integrations.").action(listIntegrations);
7
+ integrations.command("discover").description("Discover available local integrations.").action(discoverIntegrations);
8
+ integrations
9
+ .command("add <kind>")
10
+ .description("Add an integration.")
11
+ .option("--path <path>", "Override the default local path.")
12
+ .option("--disabled", "Add without enabling automatic ingestion.")
13
+ .action(addIntegration);
14
+ integrations.command("enable <source-id>").description("Enable automatic ingestion for an integration.").action((sourceId) => setEnabled(sourceId, true));
15
+ integrations.command("disable <source-id>").description("Disable automatic ingestion for an integration.").action((sourceId) => setEnabled(sourceId, false));
16
+ integrations.command("remove <source-id>").description("Remove an integration.").action(removeIntegration);
17
+ integrations.command("run").description("Run ingestion once.").action(runIngestion);
18
+ integrations.command("start").description("Enable automatic ingestion for all integrations.").action(startAutomaticIngestion);
19
+ integrations.command("stop").description("Disable automatic ingestion for all integrations.").action(stopAutomaticIngestion);
20
+ }
21
+ async function listIntegrations() {
22
+ const runtime = await ensureRuntime({ detached: true });
23
+ const { sources } = await listSources(runtime);
24
+ if (sources.length === 0) {
25
+ console.log("No integrations configured.");
26
+ return;
27
+ }
28
+ const rows = [
29
+ ["ID", "NAME", "STATUS", "PATH"],
30
+ ...sources.map((source) => [source.sourceId, source.displayName, source.enabled ? "enabled" : "disabled", source.basePath]),
31
+ ];
32
+ const colors = [
33
+ ["bold", "bold", "bold", "bold"],
34
+ ...sources.map((source) => ["dim", undefined, source.enabled ? "green" : "yellow", "cyan"]),
35
+ ];
36
+ process.stdout.write(renderTabTable(rows, colors));
37
+ }
38
+ async function discoverIntegrations() {
39
+ const runtime = await ensureRuntime({ detached: true });
40
+ const { sources } = await discoverSources(runtime);
41
+ if (sources.length === 0) {
42
+ console.log("No local integrations found.");
43
+ return;
44
+ }
45
+ const rows = [
46
+ ["KIND", "NAME", "STATUS", "PATH"],
47
+ ...sources.map((source) => [source.kind, source.displayName, source.alreadyAdded ? "configured" : "available", source.basePath]),
48
+ ];
49
+ const colors = [
50
+ ["bold", "bold", "bold", "bold"],
51
+ ...sources.map((source) => ["dim", undefined, source.alreadyAdded ? "green" : "yellow", "cyan"]),
52
+ ];
53
+ process.stdout.write(renderTabTable(rows, colors));
54
+ }
55
+ async function addIntegration(kind, options) {
56
+ const runtime = await ensureRuntime({ detached: true });
57
+ const source = await addSource(runtime, kind, options.path ?? "", options.disabled !== true, false);
58
+ console.log(`Added ${source.displayName}.`);
59
+ }
60
+ async function setEnabled(sourceId, enabled) {
61
+ const runtime = await ensureRuntime({ detached: true });
62
+ const source = await configureSource(runtime, sourceId, enabled);
63
+ console.log(`${enabled ? "Enabled" : "Disabled"} ${source.displayName}.`);
64
+ }
65
+ async function removeIntegration(sourceId) {
66
+ const runtime = await ensureRuntime({ detached: true });
67
+ await removeSource(runtime, sourceId);
68
+ console.log("Removed integration.");
69
+ }
70
+ async function runIngestion() {
71
+ const runtime = await ensureRuntime({ detached: true });
72
+ const result = await triggerIngestion(runtime);
73
+ console.log(`Ran ingestion for ${result.sourceIds.length} integration(s).`);
74
+ }
75
+ async function startAutomaticIngestion() {
76
+ const runtime = await ensureRuntime({ detached: true });
77
+ await startIngestion(runtime);
78
+ console.log("Automatic ingestion enabled.");
79
+ }
80
+ async function stopAutomaticIngestion() {
81
+ const runtime = await ensureRuntime({ detached: true });
82
+ await stopIngestion(runtime);
83
+ console.log("Automatic ingestion disabled.");
84
+ }
@@ -0,0 +1,145 @@
1
+ import { checkbox, confirm } from "@inquirer/prompts";
2
+ import { hostname } from "node:os";
3
+ import { addSource, AuthStatus, beginSignIn, discoverSources, getAuthState, listSources, listTeamInvites, respondToTeamInvite, setApiKey, startCloudSync, startIngestion } from "../nera/client.js";
4
+ import { ensureRuntime } from "../nera/process.js";
5
+ import { openBrowser } from "../platform/openBrowser.js";
6
+ import { service } from "./service.js";
7
+ import { enableCloudSyncForTeam } from "./teams.js";
8
+ import { applyTeamPoliciesForRuntime } from "./sharing.js";
9
+ export async function login(options) {
10
+ const runtime = await ensureRuntime({ detached: true });
11
+ if (runtime.started)
12
+ console.log("Started Nessie.");
13
+ const nonInteractive = options.apiKey !== undefined;
14
+ if (options.apiKey !== undefined) {
15
+ const apiKey = options.apiKey.trim();
16
+ if (!apiKey)
17
+ throw new Error("--api-key cannot be empty.");
18
+ await setApiKey(runtime, apiKey);
19
+ }
20
+ else {
21
+ const beforeAuth = await getAuthState(runtime);
22
+ if (beforeAuth.status !== AuthStatus.SIGNED_IN) {
23
+ const { url } = await beginSignIn(runtime);
24
+ if (options.open === false) {
25
+ console.log(`Open this URL to sign in: ${url}`);
26
+ }
27
+ else {
28
+ await openBrowser(url);
29
+ console.log("Opened browser sign-in.");
30
+ }
31
+ await waitForSignIn(runtime, beforeAuth);
32
+ }
33
+ }
34
+ const auth = await getAuthState(runtime);
35
+ if (auth.status !== AuthStatus.SIGNED_IN && auth.status !== AuthStatus.REAUTH_REQUIRED) {
36
+ throw new Error("Sign-in did not complete.");
37
+ }
38
+ if (auth.user?.email)
39
+ console.log(`Signed in as ${auth.user.email}.`);
40
+ if (!nonInteractive)
41
+ await handleTeamInvites(runtime);
42
+ await configureSources(runtime, { nonInteractive });
43
+ await startIngestion(runtime);
44
+ let cloudSyncStarted = false;
45
+ try {
46
+ await startCloudSync(runtime);
47
+ cloudSyncStarted = true;
48
+ }
49
+ catch (error) {
50
+ console.log(`Cloud sync was not started: ${error instanceof Error ? error.message : String(error)}`);
51
+ }
52
+ if (!nonInteractive && cloudSyncStarted) {
53
+ try {
54
+ await applyTeamPoliciesForRuntime(runtime, { quietNoop: true, retrySyncMetadata: true });
55
+ }
56
+ catch {
57
+ console.log("Team sharing policies were not applied yet. Run `nessie-daemon sharing apply-team-policies` to try again.");
58
+ }
59
+ }
60
+ const backgroundStarted = options.background !== false;
61
+ if (backgroundStarted) {
62
+ await service("start", { quiet: true });
63
+ }
64
+ console.log(successLine(backgroundStarted
65
+ ? "🎉 Nessie is set up successfully. It will run in the background to sync your integrations."
66
+ : "🎉 Nessie is set up successfully."));
67
+ }
68
+ function successLine(message) {
69
+ return process.stdout.isTTY ? `\x1b[32m${message}\x1b[0m` : message;
70
+ }
71
+ async function waitForSignIn(runtime, beforeAuth) {
72
+ const beforeAccountIds = new Set((beforeAuth.accounts ?? []).map((account) => account.id));
73
+ const alreadySignedIn = beforeAuth.status === AuthStatus.SIGNED_IN || beforeAuth.status === AuthStatus.REAUTH_REQUIRED;
74
+ const deadline = Date.now() + 5 * 60 * 1000;
75
+ while (Date.now() < deadline) {
76
+ const state = await getAuthState(runtime);
77
+ const signedIn = state.status === AuthStatus.SIGNED_IN || state.status === AuthStatus.REAUTH_REQUIRED;
78
+ const hasNewAccount = (state.accounts ?? []).some((account) => !beforeAccountIds.has(account.id));
79
+ if (signedIn && (!alreadySignedIn || hasNewAccount || state.user?.id !== beforeAuth.user?.id || state.status !== beforeAuth.status))
80
+ return;
81
+ await new Promise((resolve) => setTimeout(resolve, 1000));
82
+ }
83
+ throw new Error("Timed out waiting for browser sign-in.");
84
+ }
85
+ async function handleTeamInvites(runtime) {
86
+ const { invites } = await listTeamInvites(runtime);
87
+ let joinedTeamId;
88
+ for (const invite of invites) {
89
+ const accepted = await confirm({
90
+ message: `Join ${invite.teamName} on Nessie?`,
91
+ default: true,
92
+ });
93
+ await respondToTeamInvite(runtime, invite.teamId, accepted);
94
+ console.log(accepted ? `Joined ${invite.teamName}.` : `Declined ${invite.teamName}.`);
95
+ if (accepted)
96
+ joinedTeamId = invite.teamId;
97
+ }
98
+ // Teams share through Nessie Cloud, so joining one opts the account in;
99
+ // without this a daemon-only member can never sync (the account flag is
100
+ // latched by a first preflight the daemon otherwise refuses to run).
101
+ if (joinedTeamId !== undefined)
102
+ await enableCloudSyncForTeam(runtime, joinedTeamId);
103
+ }
104
+ async function configureSources(runtime, options) {
105
+ const [{ sources }, existingSources] = await Promise.all([discoverSources(runtime), listSources(runtime)]);
106
+ const enabledSources = new Set(existingSources.sources.filter((source) => source.enabled).map((source) => `${source.kind}:${source.basePath}`));
107
+ if (sources.length === 0) {
108
+ console.log("No supported local data integrations were found.");
109
+ return;
110
+ }
111
+ const selected = options.nonInteractive
112
+ ? sources
113
+ : await selectSources(sources, enabledSources);
114
+ // Already-enabled sources are a no-op; re-asserting them costs one RPC
115
+ // each, which the ingest-all fast path would otherwise pay on every re-run.
116
+ const changed = selected.filter((source) => !enabledSources.has(`${source.kind}:${source.basePath}`));
117
+ for (const source of changed) {
118
+ await addSource(runtime, source.kind, source.basePath, true, source.alreadyAdded);
119
+ }
120
+ const alreadyEnabled = selected.length - changed.length;
121
+ console.log(alreadyEnabled > 0
122
+ ? `Enabled ${changed.length} integration(s) (${alreadyEnabled} already enabled).`
123
+ : `Enabled ${changed.length} integration(s).`);
124
+ }
125
+ /** Fast path first: ingest everything detected; the checkbox is opt-in friction. */
126
+ async function selectSources(sources, enabledSources) {
127
+ for (const source of sources) {
128
+ console.log(` - ${source.displayName} (${source.basePath})`);
129
+ }
130
+ const ingestAll = await confirm({
131
+ message: `Ingest all ${sources.length} detected integration(s) from ${hostname()}?`,
132
+ default: true,
133
+ });
134
+ if (ingestAll)
135
+ return sources;
136
+ return checkbox({
137
+ message: "Choose integrations to ingest:",
138
+ choices: sources.map((source) => ({
139
+ name: `${source.displayName} (${source.basePath})`,
140
+ value: source,
141
+ checked: enabledSources.has(`${source.kind}:${source.basePath}`),
142
+ })),
143
+ required: true,
144
+ });
145
+ }
@@ -1,10 +1,25 @@
1
- import { runSyncLoop, runSyncOnce } from "../sync/loop.js";
2
- export async function runDaemon(options) {
1
+ import { ensureRuntime } from "../nera/process.js";
2
+ import { startCloudSync, startIngestion, triggerCloudSync, triggerIngestion } from "../nera/client.js";
3
+ export async function run(options) {
3
4
  if (options.once) {
4
- const result = await runSyncOnce();
5
- console.log(`Synced ${result.nodes} nodes and ${result.edges} edges.`);
5
+ const runtime = await ensureRuntime({ detached: true });
6
+ const result = await triggerIngestion(runtime);
7
+ const cloudSyncQueued = await tryCloudSync(() => triggerCloudSync(runtime));
8
+ console.log(`Triggered Nessie ingestion for ${result.sourceIds.length} integration(s)${cloudSyncQueued ? " and queued cloud sync" : ""}.`);
6
9
  return;
7
10
  }
8
- const intervalSeconds = Number.parseInt(options.intervalSeconds ?? "60", 10);
9
- await runSyncLoop(Number.isFinite(intervalSeconds) ? intervalSeconds : 60);
11
+ const runtime = await ensureRuntime({ detached: true });
12
+ await startIngestion(runtime);
13
+ await tryCloudSync(() => startCloudSync(runtime));
14
+ console.log(runtime.started ? "Started Nessie." : "Nessie is already running.");
15
+ }
16
+ async function tryCloudSync(action) {
17
+ try {
18
+ await action();
19
+ return true;
20
+ }
21
+ catch (error) {
22
+ console.log(`Cloud sync was not started: ${error instanceof Error ? error.message : String(error)}`);
23
+ return false;
24
+ }
10
25
  }
@@ -1,45 +1,27 @@
1
- import { readConfig } from "../config/store.js";
2
- import { backgroundDaemonStatus, startBackgroundDaemon, stopBackgroundDaemon } from "../service/process.js";
3
- import { systemctl } from "../service/systemd.js";
4
- const defaultServiceDeps = {
5
- backgroundDaemonStatus,
6
- startBackgroundDaemon,
7
- stopBackgroundDaemon,
8
- systemctl,
9
- readConfig,
10
- };
11
- export async function runServiceCommand(command, options = {}, depsOverride = {}) {
12
- const deps = { ...defaultServiceDeps, ...depsOverride };
13
- if (command === "start" && await shouldStartBackground(options, deps)) {
14
- console.log(await deps.startBackgroundDaemon());
15
- return;
1
+ import { startCloudSync, startIngestion } from "../nera/client.js";
2
+ import { ensureRuntime, probeRuntime, stopRuntime } from "../nera/process.js";
3
+ export async function service(command, options = {}) {
4
+ if (command === "start") {
5
+ const runtime = await ensureRuntime({ detached: true });
6
+ await startIngestion(runtime);
7
+ await tryCloudSync(() => startCloudSync(runtime), options.quiet);
8
+ return "";
16
9
  }
17
10
  if (command === "stop") {
18
- const status = await deps.backgroundDaemonStatus();
19
- if (status !== null) {
20
- console.log(await deps.stopBackgroundDaemon());
21
- return;
22
- }
11
+ const stopped = await stopRuntime();
12
+ return stopped ? "Stopped Nessie." : "Nessie is not running.";
23
13
  }
24
- if (command === "status") {
25
- const status = await deps.backgroundDaemonStatus();
26
- if (status !== null) {
27
- console.log(status);
28
- return;
29
- }
30
- }
31
- const output = await deps.systemctl(command, "nessie-daemon.service");
32
- if (output.trim())
33
- console.log(output.trim());
14
+ const runtime = await probeRuntime();
15
+ return runtime ? "Nessie is running." : "Nessie is not running.";
34
16
  }
35
- async function shouldStartBackground(options, deps) {
36
- if (options.background)
37
- return true;
17
+ async function tryCloudSync(action, quiet) {
38
18
  try {
39
- const config = await deps.readConfig();
40
- return Boolean(config.apiKey);
19
+ await action();
20
+ return true;
41
21
  }
42
- catch {
22
+ catch (error) {
23
+ if (!quiet)
24
+ console.log(`Cloud sync was not started: ${error instanceof Error ? error.message : String(error)}`);
43
25
  return false;
44
26
  }
45
27
  }