@freesyntax/notch-cli 0.4.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.
package/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ Copyright 2026 Driftrail (driftrail.com)
8
+
9
+ Licensed under the Apache License, Version 2.0 (the "License");
10
+ you may not use this file except in compliance with the License.
11
+ You may obtain a copy of the License at
12
+
13
+ http://www.apache.org/licenses/LICENSE-2.0
14
+
15
+ Unless required by applicable law or agreed to in writing, software
16
+ distributed under the License is distributed on an "AS IS" BASIS,
17
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
+ See the License for the specific language governing permissions and
19
+ limitations under the License.
@@ -0,0 +1,16 @@
1
+ import {
2
+ clearCredentials,
3
+ getConfigDir,
4
+ getCredentialsPath,
5
+ loadCredentials,
6
+ login,
7
+ saveCredentials
8
+ } from "./chunk-TJS4W4R5.js";
9
+ export {
10
+ clearCredentials,
11
+ getConfigDir,
12
+ getCredentialsPath,
13
+ loadCredentials,
14
+ login,
15
+ saveCredentials
16
+ };
@@ -0,0 +1,142 @@
1
+ // src/agent/compression.ts
2
+ import { generateText } from "ai";
3
+ function estimateTokens(messages) {
4
+ let chars = 0;
5
+ for (const msg of messages) {
6
+ if (typeof msg.content === "string") {
7
+ chars += msg.content.length;
8
+ } else if (Array.isArray(msg.content)) {
9
+ for (const part of msg.content) {
10
+ if ("text" in part) chars += part.text.length;
11
+ else if ("result" in part) chars += JSON.stringify(part.result).length;
12
+ else if ("args" in part) chars += JSON.stringify(part.args).length;
13
+ }
14
+ }
15
+ }
16
+ return Math.ceil(chars / 4);
17
+ }
18
+ async function compressHistory(messages, opts) {
19
+ const threshold = opts.contextWindow * 0.75;
20
+ const currentTokens = estimateTokens(messages);
21
+ if (currentTokens < threshold || messages.length < 6) {
22
+ return { messages, compressed: false };
23
+ }
24
+ const keepRecent = opts.keepRecent ?? 4;
25
+ const keepStart = 1;
26
+ const head = messages.slice(0, keepStart);
27
+ const middle = messages.slice(keepStart, -keepRecent);
28
+ const tail = messages.slice(-keepRecent);
29
+ if (middle.length === 0) {
30
+ return { messages, compressed: false };
31
+ }
32
+ const middleSummary = summarizeMessages(middle);
33
+ let summaryText;
34
+ try {
35
+ const result = await generateText({
36
+ model: opts.model,
37
+ system: "You are a conversation summarizer. Condense the following conversation history into a brief summary that preserves all important context, decisions made, files modified, and any errors encountered. Be concise but thorough. Output only the summary.",
38
+ messages: [{ role: "user", content: middleSummary }],
39
+ maxTokens: 1024
40
+ });
41
+ summaryText = result.text;
42
+ } catch {
43
+ summaryText = buildDeterministicSummary(middle);
44
+ }
45
+ const compressedMessages = [...head];
46
+ const summaryContent = `[Previous conversation context]
47
+ ${summaryText}
48
+ [End of context]`;
49
+ if (tail.length > 0 && tail[0].role === "user") {
50
+ const firstContent = typeof tail[0].content === "string" ? tail[0].content : "";
51
+ compressedMessages.push({
52
+ role: "user",
53
+ content: `${summaryContent}
54
+
55
+ ---
56
+
57
+ ${firstContent}`
58
+ });
59
+ compressedMessages.push(...tail.slice(1));
60
+ } else {
61
+ compressedMessages.push({ role: "user", content: summaryContent });
62
+ compressedMessages.push({
63
+ role: "assistant",
64
+ content: "Understood. I have the context from our previous conversation. Continuing."
65
+ });
66
+ compressedMessages.push(...tail);
67
+ }
68
+ return { messages: compressedMessages, compressed: true };
69
+ }
70
+ function summarizeMessages(messages) {
71
+ const lines = [];
72
+ for (const msg of messages) {
73
+ const role = msg.role.toUpperCase();
74
+ if (typeof msg.content === "string") {
75
+ lines.push(`${role}: ${msg.content.slice(0, 500)}`);
76
+ } else if (Array.isArray(msg.content)) {
77
+ const parts = [];
78
+ for (const part of msg.content) {
79
+ if ("text" in part) parts.push(part.text.slice(0, 200));
80
+ else if ("toolName" in part) parts.push(`[tool: ${part.toolName}]`);
81
+ else if ("result" in part) parts.push(`[result: ${JSON.stringify(part.result).slice(0, 100)}]`);
82
+ }
83
+ lines.push(`${role}: ${parts.join(" | ")}`);
84
+ }
85
+ }
86
+ return lines.join("\n");
87
+ }
88
+ function buildDeterministicSummary(messages) {
89
+ const filesModified = /* @__PURE__ */ new Set();
90
+ const toolsUsed = /* @__PURE__ */ new Set();
91
+ const userRequests = [];
92
+ let errorCount = 0;
93
+ for (const msg of messages) {
94
+ if (msg.role === "user" && typeof msg.content === "string") {
95
+ userRequests.push(msg.content.slice(0, 100));
96
+ }
97
+ if (Array.isArray(msg.content)) {
98
+ for (const part of msg.content) {
99
+ if ("toolName" in part) {
100
+ const p = part;
101
+ toolsUsed.add(p.toolName);
102
+ if (p.args?.path) filesModified.add(String(p.args.path));
103
+ }
104
+ if ("result" in part) {
105
+ const r = part;
106
+ if (r.result?.isError) errorCount++;
107
+ }
108
+ }
109
+ }
110
+ }
111
+ const lines = ["Summary of previous conversation:"];
112
+ if (userRequests.length > 0) {
113
+ lines.push(`- User requests: ${userRequests.join("; ")}`);
114
+ }
115
+ if (toolsUsed.size > 0) {
116
+ lines.push(`- Tools used: ${[...toolsUsed].join(", ")}`);
117
+ }
118
+ if (filesModified.size > 0) {
119
+ lines.push(`- Files touched: ${[...filesModified].join(", ")}`);
120
+ }
121
+ if (errorCount > 0) {
122
+ lines.push(`- Errors encountered: ${errorCount}`);
123
+ }
124
+ lines.push(`- Total messages summarized: ${messages.length}`);
125
+ return lines.join("\n");
126
+ }
127
+ async function autoCompress(messages, model, contextWindow, onCompress) {
128
+ const result = await compressHistory(messages, {
129
+ model,
130
+ contextWindow
131
+ });
132
+ if (result.compressed) {
133
+ onCompress?.();
134
+ }
135
+ return result.messages;
136
+ }
137
+
138
+ export {
139
+ estimateTokens,
140
+ compressHistory,
141
+ autoCompress
142
+ };
@@ -0,0 +1,176 @@
1
+ // src/auth.ts
2
+ import http from "http";
3
+ import { exec } from "child_process";
4
+ import os from "os";
5
+ import path from "path";
6
+ import fs from "fs/promises";
7
+ var FREESYNTAX_URL = "https://freesyntax.dev";
8
+ var PREFERRED_PORT = 9721;
9
+ var AUTH_TIMEOUT_MS = 5 * 60 * 1e3;
10
+ function getConfigDir() {
11
+ if (process.platform === "win32") {
12
+ return path.join(process.env["APPDATA"] ?? os.homedir(), "notch");
13
+ }
14
+ const xdg = process.env["XDG_CONFIG_HOME"];
15
+ return path.join(xdg ?? path.join(os.homedir(), ".config"), "notch");
16
+ }
17
+ function getCredentialsPath() {
18
+ return path.join(getConfigDir(), "credentials.json");
19
+ }
20
+ async function loadCredentials() {
21
+ try {
22
+ const raw = await fs.readFile(getCredentialsPath(), "utf-8");
23
+ const parsed = JSON.parse(raw);
24
+ if (typeof parsed.token === "string" && typeof parsed.email === "string") {
25
+ return parsed;
26
+ }
27
+ return null;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+ async function saveCredentials(token, email) {
33
+ const dir = getConfigDir();
34
+ await fs.mkdir(dir, { recursive: true });
35
+ const credPath = getCredentialsPath();
36
+ const creds = { token, email, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
37
+ await fs.writeFile(credPath, JSON.stringify(creds, null, 2), {
38
+ encoding: "utf-8",
39
+ mode: 384
40
+ });
41
+ }
42
+ async function clearCredentials() {
43
+ try {
44
+ await fs.unlink(getCredentialsPath());
45
+ } catch {
46
+ }
47
+ }
48
+ function openBrowser(url) {
49
+ let cmd;
50
+ if (process.platform === "win32") {
51
+ cmd = `start "" "${url}"`;
52
+ } else if (process.platform === "darwin") {
53
+ cmd = `open "${url}"`;
54
+ } else {
55
+ cmd = `xdg-open "${url}"`;
56
+ }
57
+ exec(cmd, () => {
58
+ });
59
+ }
60
+ async function findFreePort() {
61
+ return new Promise((resolve) => {
62
+ const server = http.createServer();
63
+ server.listen(PREFERRED_PORT, () => {
64
+ const port = server.address().port;
65
+ server.close(() => resolve(port));
66
+ });
67
+ server.on("error", () => {
68
+ const fallback = http.createServer();
69
+ fallback.listen(0, () => {
70
+ const port = fallback.address().port;
71
+ fallback.close(() => resolve(port));
72
+ });
73
+ });
74
+ });
75
+ }
76
+ function generateState() {
77
+ const bytes = new Uint8Array(16);
78
+ crypto.getRandomValues(bytes);
79
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
80
+ }
81
+ async function login() {
82
+ const state = generateState();
83
+ const port = await findFreePort();
84
+ return new Promise((resolve, reject) => {
85
+ let settled = false;
86
+ const finish = (fn) => {
87
+ if (!settled) {
88
+ settled = true;
89
+ server.close(fn);
90
+ }
91
+ };
92
+ const server = http.createServer((req, res) => {
93
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
94
+ if (url.pathname !== "/callback") {
95
+ res.writeHead(404).end();
96
+ return;
97
+ }
98
+ const receivedState = url.searchParams.get("state");
99
+ const token = url.searchParams.get("token");
100
+ const email = url.searchParams.get("email");
101
+ const error = url.searchParams.get("error");
102
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
103
+ if (error || receivedState !== state || !token || !email) {
104
+ res.end(HTML_ERROR);
105
+ finish(() => reject(new Error(error ?? "Authentication failed. Please try again.")));
106
+ return;
107
+ }
108
+ res.end(HTML_SUCCESS);
109
+ finish(async () => {
110
+ await saveCredentials(token, email);
111
+ resolve({ token, email, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
112
+ });
113
+ });
114
+ server.on("error", (err) => {
115
+ finish(() => reject(new Error(`Failed to start local server: ${err.message}`)));
116
+ });
117
+ server.listen(port, () => {
118
+ const authUrl = `${FREESYNTAX_URL}/cli-auth?port=${port}&state=${state}`;
119
+ openBrowser(authUrl);
120
+ process.stdout.write(`
121
+ If browser did not open automatically, visit:
122
+ ${authUrl}
123
+
124
+ `);
125
+ });
126
+ setTimeout(() => {
127
+ finish(() => reject(new Error("Authentication timed out after 5 minutes.")));
128
+ }, AUTH_TIMEOUT_MS);
129
+ });
130
+ }
131
+ var HTML_BASE = (body) => `<!DOCTYPE html>
132
+ <html lang="en">
133
+ <head>
134
+ <meta charset="utf-8">
135
+ <meta name="viewport" content="width=device-width, initial-scale=1">
136
+ <title>Notch CLI</title>
137
+ <link rel="preconnect" href="https://fonts.googleapis.com">
138
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@200;400&display=swap" rel="stylesheet">
139
+ <style>
140
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
141
+ body {
142
+ font-family: 'IBM Plex Mono', monospace;
143
+ background: #000;
144
+ color: #cfcfcf;
145
+ display: flex;
146
+ align-items: center;
147
+ justify-content: center;
148
+ min-height: 100vh;
149
+ }
150
+ .card { text-align: center; }
151
+ .brand { color: #CE2127; font-size: 11px; letter-spacing: 0.3em; text-transform: uppercase; }
152
+ h1 { font-weight: 200; font-size: 22px; margin: 14px 0 10px; letter-spacing: 0.05em; }
153
+ p { font-size: 11px; color: #555; letter-spacing: 0.1em; line-height: 1.8; }
154
+ </style>
155
+ </head>
156
+ <body><div class="card">${body}</div></body>
157
+ </html>`;
158
+ var HTML_SUCCESS = HTML_BASE(`
159
+ <div class="brand">notch_</div>
160
+ <h1>CLI authorized</h1>
161
+ <p>You can close this tab and return to your terminal.</p>
162
+ `);
163
+ var HTML_ERROR = HTML_BASE(`
164
+ <div class="brand">notch_</div>
165
+ <h1>Authorization failed</h1>
166
+ <p style="color:#CE2127">Run <code>notch login</code> again to retry.</p>
167
+ `);
168
+
169
+ export {
170
+ getConfigDir,
171
+ getCredentialsPath,
172
+ loadCredentials,
173
+ saveCredentials,
174
+ clearCredentials,
175
+ login
176
+ };
@@ -0,0 +1,10 @@
1
+ import {
2
+ autoCompress,
3
+ compressHistory,
4
+ estimateTokens
5
+ } from "./chunk-MWM5TFY4.js";
6
+ export {
7
+ autoCompress,
8
+ compressHistory,
9
+ estimateTokens
10
+ };