neuvikon 0.2.5 → 0.2.7

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
@@ -74,7 +74,7 @@ being used, add the block above by hand.
74
74
  | | |
75
75
  |---|---|
76
76
  | `~/.neuvikon/config.json` | Token and deployment URL, `0600` |
77
- | `.neuvikon-session` | Which ticket this directory is tracking. Git-ignored by `init` |
77
+ | `.neuvikon-session[.<id>]` | Which ticket this directory (or, under Claude Code, this agent) is tracking. Git-ignored by `init` |
78
78
  | `--dev` | Target the development deployment instead of production |
79
79
  | `NEUVIKON_URL` | Point at a self-hosted deployment. `--url` does the same per command |
80
80
  | `NEUVIKON_TOKEN` | Use this token instead of logging in — for CI and anywhere a browser cannot open |
package/dist/index.mjs CHANGED
@@ -156,17 +156,32 @@ function writeCursorRule(root) {
156
156
  action: existing ? "updated" : "created"
157
157
  };
158
158
  }
159
- /** Keeps the per-directory session file out of the user's commits. */
159
+ /**
160
+ * Keeps the per-directory session file(s) out of the user's commits. Globbed
161
+ * because each concurrent agent gets its own `.neuvikon-session.<id>` file
162
+ * now (see `session.ts`) — an exact-match `.neuvikon-session` line no longer
163
+ * covers them, so an older, already-ignored project is migrated in place.
164
+ */
160
165
  function ignoreSessionFile(root) {
161
166
  const path = join(root, ".gitignore");
162
167
  if (!existsSync(join(root, ".git")) && !existsSync(path)) return null;
163
168
  const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
164
- if (existing.split("\n").some((line) => line.trim() === ".neuvikon-session")) return {
169
+ const lines = existing.split("\n");
170
+ if (lines.some((line) => line.trim() === ".neuvikon-session*")) return {
165
171
  path: ".gitignore",
166
172
  action: "unchanged"
167
173
  };
174
+ const legacyIndex = lines.findIndex((line) => line.trim() === ".neuvikon-session");
175
+ if (legacyIndex !== -1) {
176
+ lines[legacyIndex] = ".neuvikon-session*";
177
+ writeFileSync(path, lines.join("\n"));
178
+ return {
179
+ path: ".gitignore",
180
+ action: "updated"
181
+ };
182
+ }
168
183
  const prefix = existing.length === 0 ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
169
- writeFileSync(path, `${existing}${prefix}# neuvikon CLI session state\n.neuvikon-session\n`);
184
+ writeFileSync(path, `${existing}${prefix}# neuvikon CLI session state\n.neuvikon-session*\n`);
170
185
  return {
171
186
  path: ".gitignore",
172
187
  action: existing ? "updated" : "created"
@@ -452,11 +467,25 @@ async function login(flags) {
452
467
  //#endregion
453
468
  //#region src/session.ts
454
469
  /**
470
+ * Distinguishes concurrent agents working in the *same* checkout — two Claude
471
+ * Code panes both running `claude` in this directory get two session files,
472
+ * because each one's process tree carries its own CLAUDE_CODE_SESSION_ID. A
473
+ * tool that doesn't set it (or a person typing the command by hand) falls
474
+ * back to the pre-multi-agent behavior of one session per directory.
475
+ */
476
+ function instanceKey() {
477
+ return process.env.CLAUDE_CODE_SESSION_ID?.trim() || null;
478
+ }
479
+ /**
455
480
  * Scoped to the working directory rather than the machine: two checkouts driven
456
- * in parallel are two sessions, which is what running two agents means.
481
+ * in parallel are two sessions, which is what running two agents means. Within
482
+ * one checkout, `instanceKey()` further scopes it per agent so two panes in the
483
+ * same directory don't stop each other's session out from under them.
457
484
  */
458
485
  function sessionPath() {
459
- return join(process.cwd(), ".neuvikon-session");
486
+ const key = instanceKey();
487
+ const filename = key ? `.neuvikon-session.${key}` : ".neuvikon-session";
488
+ return join(process.cwd(), filename);
460
489
  }
461
490
  function readSession() {
462
491
  let raw;
@@ -555,6 +584,25 @@ async function readStdin() {
555
584
  * while we read it will have a torn final line, and that is normal, not an
556
585
  * error worth failing a user's turn over.
557
586
  */
587
+ /**
588
+ * Where the transcript currently ends, so a session that starts mid-conversation
589
+ * can mark its starting point instead of counting what came before it.
590
+ */
591
+ function transcriptSize(path) {
592
+ let fd;
593
+ try {
594
+ fd = openSync(path, "r");
595
+ } catch {
596
+ return null;
597
+ }
598
+ try {
599
+ return fstatSync(fd).size;
600
+ } catch {
601
+ return null;
602
+ } finally {
603
+ closeSync(fd);
604
+ }
605
+ }
558
606
  function readUsageSince(path, offset) {
559
607
  let fd;
560
608
  try {
@@ -661,7 +709,12 @@ async function report(flags) {
661
709
  let nextOffset;
662
710
  const transcript = flags.get("transcript") || await transcriptFromHookPayload();
663
711
  if (transcript) {
664
- const usage = readUsageSince(transcript, session.transcriptOffset ?? 0);
712
+ const baseline = session.transcriptOffset ?? transcriptSize(transcript);
713
+ if (session.transcriptOffset === void 0 && baseline !== null) writeSession({
714
+ ...session,
715
+ transcriptOffset: baseline
716
+ });
717
+ const usage = baseline === null ? null : readUsageSince(transcript, baseline);
665
718
  if (usage) {
666
719
  tokensIn = (tokensIn ?? 0) + usage.tokensIn;
667
720
  tokensOut = (tokensOut ?? 0) + usage.tokensOut;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuvikon",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Tell Neuvikon which ticket your coding agent is working on.",
5
5
  "keywords": [
6
6
  "neuvikon",