@causyne/agent 1.10.6 → 1.10.12

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
@@ -12,11 +12,29 @@ documentation is intentionally concise while the project is evolving.
12
12
 
13
13
  Learn more at [causyne.com](https://causyne.com/).
14
14
 
15
+ ## Install
16
+
17
+ On macOS, Linux, or WSL:
18
+
19
+ ```sh
20
+ curl -fsSL https://causyne.com/install.sh | bash
21
+ ```
22
+
23
+ On Windows PowerShell:
24
+
25
+ ```powershell
26
+ irm https://causyne.com/install.ps1 | iex
27
+ ```
28
+
29
+ Open a project directory and run `causyne`. Managed installations check the
30
+ official npm release channel for updates. Run `causyne doctor` to inspect the
31
+ runtime and update channel.
32
+
15
33
  ## Contact
16
34
 
17
35
  - General inquiries and business communication: [contact@causyne.com](mailto:contact@causyne.com)
18
36
  - Product support and troubleshooting: [help@causyne.com](mailto:help@causyne.com)
19
- - Partnerships and direct founder communication: [ikbal@causyne.com](mailto:ikbal@causyne.com)
37
+ - Partnerships and direct communication: [ikbal@causyne.com](mailto:ikbal@causyne.com)
20
38
 
21
39
  ## Development
22
40
 
package/bin/causyne.js CHANGED
@@ -5,6 +5,11 @@ if (Number(process.versions.node.split('.')[0]) < 24) {
5
5
  process.exit(1);
6
6
  }
7
7
  try {
8
+ const { startUpdateChecker } = await import('../src/autoUpdate.js');
9
+ const update = await startUpdateChecker();
10
+ if (update && update.updated) {
11
+ process.stderr.write(`Causyne updated to ${update.latest}. Starting the new version.\n`);
12
+ }
8
13
  await import('../src/entrypoint.js');
9
14
  } catch (error) {
10
15
  if (process.stdout.isTTY) process.stdout.write('\x1b[?1006l\x1b[?1000l\x1b[?2004l\x1b[?25h');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@causyne/agent",
3
- "version": "1.10.6",
3
+ "version": "1.10.12",
4
4
  "description": "Agentic AI for software, tools, and computer workflows",
5
5
  "main": "bin/causyne.js",
6
6
  "type": "module",
package/src/autoUpdate.js CHANGED
@@ -1,12 +1,226 @@
1
+ // Causyne package updates.
2
+ //
3
+ // Updates are intentionally limited to installations managed by npm or by the
4
+ // causyne.com installer. A source checkout is never rewritten. Commands are
5
+ // passed as argv arrays with shell disabled, and the only accepted package is
6
+ // the public @causyne/agent package from the npm registry.
7
+
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { spawnSync } from 'node:child_process';
12
+
1
13
  import { UI } from './utils/cli_ui.js';
14
+ import { causyneHome } from './utils/paths.js';
15
+ import { getPackageVersion } from './version.js';
16
+
17
+ const PACKAGE_NAME = '@causyne/agent';
18
+ const REGISTRY_LATEST = 'https://registry.npmjs.org/@causyne%2fagent/latest';
19
+ const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
20
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
21
+
22
+ function updateStateFile(options = {}) {
23
+ if (options.stateFile) return path.resolve(options.stateFile);
24
+ const env = options.env || process.env;
25
+ const root = env.CAUSYNE_HOME && String(env.CAUSYNE_HOME).trim()
26
+ ? path.resolve(String(env.CAUSYNE_HOME).trim())
27
+ : causyneHome();
28
+ return path.join(root, 'update-state.json');
29
+ }
30
+
31
+ function versionParts(version) {
32
+ const match = String(version || '').trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
33
+ if (!match) return null;
34
+ return { numbers: match.slice(1, 4).map(Number), prerelease: match[4] || '' };
35
+ }
36
+
37
+ export function compareVersions(left, right) {
38
+ const a = versionParts(left);
39
+ const b = versionParts(right);
40
+ if (!a || !b) return 0;
41
+ for (let index = 0; index < 3; index++) {
42
+ if (a.numbers[index] !== b.numbers[index]) return a.numbers[index] > b.numbers[index] ? 1 : -1;
43
+ }
44
+ if (a.prerelease === b.prerelease) return 0;
45
+ if (!a.prerelease) return 1;
46
+ if (!b.prerelease) return -1;
47
+ const aParts = a.prerelease.split('.');
48
+ const bParts = b.prerelease.split('.');
49
+ const length = Math.max(aParts.length, bParts.length);
50
+ for (let index = 0; index < length; index++) {
51
+ if (aParts[index] === undefined) return -1;
52
+ if (bParts[index] === undefined) return 1;
53
+ if (aParts[index] === bParts[index]) continue;
54
+ const aNumeric = /^\d+$/.test(aParts[index]);
55
+ const bNumeric = /^\d+$/.test(bParts[index]);
56
+ if (aNumeric && bNumeric) return Number(aParts[index]) > Number(bParts[index]) ? 1 : -1;
57
+ if (aNumeric !== bNumeric) return aNumeric ? -1 : 1;
58
+ return aParts[index].localeCompare(bParts[index]);
59
+ }
60
+ return 0;
61
+ }
62
+
63
+ function managedPrefix(packageRoot = PACKAGE_ROOT, env = process.env) {
64
+ if (env.CAUSYNE_INSTALL_ROOT) return path.resolve(env.CAUSYNE_INSTALL_ROOT);
65
+
66
+ const markers = [
67
+ path.join('lib', 'node_modules', '@causyne', 'agent'),
68
+ path.join('node_modules', '@causyne', 'agent')
69
+ ];
70
+ for (const marker of markers) {
71
+ const index = packageRoot.lastIndexOf(marker);
72
+ if (index >= 0 && index + marker.length === packageRoot.length) {
73
+ return packageRoot.slice(0, index).replace(/[\\/]$/, '');
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+
79
+ export function installationInfo(options = {}) {
80
+ const env = options.env || process.env;
81
+ const packageRoot = options.packageRoot || PACKAGE_ROOT;
82
+ const prefix = managedPrefix(packageRoot, env);
83
+ return {
84
+ managed: Boolean(prefix),
85
+ type: env.CAUSYNE_INSTALL_ROOT ? 'causyne.com installer' : (prefix ? 'global npm' : 'source checkout'),
86
+ prefix,
87
+ packageRoot,
88
+ npmCommand: env.CAUSYNE_NPM_COMMAND || (process.platform === 'win32' ? 'npm.cmd' : 'npm')
89
+ };
90
+ }
91
+
92
+ function readState(options = {}) {
93
+ try {
94
+ return JSON.parse(fs.readFileSync(updateStateFile(options), 'utf8'));
95
+ } catch {
96
+ return {};
97
+ }
98
+ }
99
+
100
+ function writeState(state, options = {}) {
101
+ try {
102
+ const stateFile = updateStateFile(options);
103
+ fs.mkdirSync(path.dirname(stateFile), { recursive: true, mode: 0o700 });
104
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), { mode: 0o600 });
105
+ } catch {
106
+ // Update state is an optimisation. Failure must never stop the CLI.
107
+ }
108
+ }
109
+
110
+ export async function latestPackageVersion(options = {}) {
111
+ const fetchImpl = options.fetchImpl || globalThis.fetch;
112
+ if (typeof fetchImpl !== 'function') throw new Error('This Node.js runtime has no fetch implementation.');
113
+ const timeoutMs = options.timeoutMs || 3500;
114
+ const signal = options.signal || AbortSignal.timeout(timeoutMs);
115
+ const response = await fetchImpl(REGISTRY_LATEST, {
116
+ headers: { accept: 'application/json', 'user-agent': `causyne/${getPackageVersion()}` },
117
+ signal
118
+ });
119
+ if (!response.ok) throw new Error(`npm registry returned HTTP ${response.status}`);
120
+ const metadata = await response.json();
121
+ if (metadata.name !== PACKAGE_NAME || !versionParts(metadata.version)) {
122
+ throw new Error('npm registry returned invalid package metadata');
123
+ }
124
+ return metadata.version;
125
+ }
126
+
127
+ function installLatest(info, options = {}) {
128
+ const runner = options.spawnSyncImpl || spawnSync;
129
+ const args = [
130
+ 'install', '--global', '--prefix', info.prefix,
131
+ `${PACKAGE_NAME}@${options.version}`, '--ignore-scripts', '--no-audit', '--no-fund'
132
+ ];
133
+ const result = runner(info.npmCommand, args, {
134
+ shell: false,
135
+ stdio: options.verbose ? 'inherit' : 'pipe',
136
+ encoding: 'utf8',
137
+ env: options.env || process.env
138
+ });
139
+ if (result.error) throw result.error;
140
+ if (result.status !== 0) {
141
+ const detail = String(result.stderr || result.stdout || '').trim();
142
+ throw new Error(detail || `npm exited with status ${result.status}`);
143
+ }
144
+ }
145
+
146
+ export async function checkForUpdate(options = {}) {
147
+ const current = options.currentVersion || getPackageVersion();
148
+ const latest = await latestPackageVersion(options);
149
+ return { current, latest, available: compareVersions(latest, current) > 0 };
150
+ }
151
+
152
+ export async function runSelfUpdate(verbose = false, options = {}) {
153
+ const info = installationInfo(options);
154
+ if (!info.managed) {
155
+ if (verbose) UI.printInfo('This is a source checkout. Update it with Git; Causyne will not rewrite developer source.');
156
+ return { updated: false, reason: 'source-checkout', ...info };
157
+ }
158
+
159
+ try {
160
+ const status = await checkForUpdate(options);
161
+ if (!status.available) {
162
+ writeState({ checkedAt: Date.now(), latest: status.latest }, options);
163
+ if (verbose) UI.printSuccess(`Causyne ${status.current} is up to date.`);
164
+ return { updated: false, reason: 'current', ...status, ...info };
165
+ }
166
+
167
+ if (verbose) UI.printInfo(`Updating Causyne ${status.current} → ${status.latest}...`);
168
+ installLatest(info, { ...options, version: status.latest, verbose });
169
+ writeState({ checkedAt: Date.now(), latest: status.latest, installedAt: Date.now() }, options);
170
+ if (verbose) UI.printSuccess(`Updated to Causyne ${status.latest}.`);
171
+ return { updated: true, ...status, ...info };
172
+ } catch (error) {
173
+ writeState({ checkedAt: Date.now(), error: error.message }, options);
174
+ if (verbose) UI.printError(`Update failed: ${error.message}`);
175
+ return { updated: false, reason: 'failed', error: error.message, ...info };
176
+ }
177
+ }
178
+
179
+ /** Check on launch, at most once every six hours. */
180
+ export async function startUpdateChecker(options = {}) {
181
+ const env = options.env || process.env;
182
+ if (env.CAUSYNE_DISABLE_AUTOUPDATE === '1' || env.CAUSYNE_DISABLE_AUTOUPDATE === 'true') {
183
+ return { updated: false, reason: 'disabled' };
184
+ }
185
+
186
+ const info = installationInfo(options);
187
+ if (!info.managed) return { updated: false, reason: 'source-checkout', ...info };
188
+
189
+ const state = readState(options);
190
+ if (!options.force && Number.isFinite(state.checkedAt)
191
+ && Date.now() - state.checkedAt < CHECK_INTERVAL_MS) {
192
+ return { updated: false, reason: 'recently-checked', latest: state.latest, ...info };
193
+ }
194
+
195
+ return runSelfUpdate(false, options);
196
+ }
2
197
 
3
- // No registry polling or self-modifying installation. A release channel has
4
- // not been selected or published for the independent Causyne project yet.
5
- export async function runSelfUpdate(verbose = false) {
6
- if (verbose) UI.printInfo('Automatic updates are disabled. Review and update your Causyne source checkout explicitly. No public package is configured yet.');
7
- return false;
198
+ export async function diagnoseInstallation(options = {}) {
199
+ const info = installationInfo(options);
200
+ let update;
201
+ try {
202
+ update = await checkForUpdate(options);
203
+ } catch (error) {
204
+ update = { current: getPackageVersion(), latest: null, available: false, error: error.message };
205
+ }
206
+ return {
207
+ ...info,
208
+ ...update,
209
+ autoUpdates: !['1', 'true'].includes(String((options.env || process.env).CAUSYNE_DISABLE_AUTOUPDATE || '').toLowerCase())
210
+ };
8
211
  }
9
212
 
10
- export function startUpdateChecker() {
11
- // Compatibility with the local CLI caller; deliberately no timer or I/O.
213
+ export function formatDoctorReport(status, runtime = process) {
214
+ const rows = [
215
+ `Version : ${status.current}`,
216
+ `Latest : ${status.latest || `unavailable (${status.error || 'unknown error'})`}`,
217
+ `Installation : ${status.type}`,
218
+ `Package root : ${status.packageRoot}`,
219
+ `Node.js : ${runtime.version}`,
220
+ `Platform : ${runtime.platform} ${runtime.arch}`,
221
+ `Auto updates : ${status.autoUpdates ? 'enabled' : 'disabled'}`,
222
+ `Workspace : ${runtime.cwd()}`
223
+ ];
224
+ if (status.available) rows.push('Update : available — run `causyne update`');
225
+ return rows.join('\n');
12
226
  }
package/src/entrypoint.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { program } from 'commander';
2
2
 
3
3
  import { runTask, chatMode, execCommand, interactiveConfig } from './cliMode.js';
4
- import { runSelfUpdate } from './autoUpdate.js';
4
+ import { runSelfUpdate, diagnoseInstallation, formatDoctorReport } from './autoUpdate.js';
5
5
  import { printContextReport, printUsageReport, printCheckpoints, printUndo, buildStatusText, printReport } from './reporting.js';
6
6
  import { runCommand } from './commands.js';
7
7
  import { resolveInvocation } from './tui/invocation.js';
@@ -70,11 +70,19 @@ program
70
70
 
71
71
  program
72
72
  .command('update')
73
- .description('Show source update guidance (no automatic installation)')
73
+ .description('Check for and install the latest Causyne Agent release')
74
74
  .action(async () => {
75
75
  await runSelfUpdate(true);
76
76
  });
77
77
 
78
+ program
79
+ .command('doctor')
80
+ .description('Inspect the installation, runtime and update channel')
81
+ .action(async () => {
82
+ const status = await diagnoseInstallation();
83
+ printReport(formatDoctorReport(status), { title: 'Doctor' });
84
+ });
85
+
78
86
  program
79
87
  .command('status')
80
88
  .description('Show local provider, workspace and runtime status')
@@ -164,6 +164,13 @@ export function initShadow(worktree) {
164
164
  }
165
165
  }
166
166
 
167
+ // A checkpoint promises the exact bytes that were present when it was
168
+ // captured. Git for Windows commonly inherits core.autocrlf=true from the
169
+ // host, which would turn an LF blob into CRLF during restore. This shadow
170
+ // repository is a byte store, so text conversion must stay disabled even
171
+ // when an existing shadow is reused after an upgrade.
172
+ runGit(['--git-dir', gitDir, 'config', 'core.autocrlf', 'false']);
173
+
167
174
  // Point at the user's object database so blobs are never re-hashed, and adopt
168
175
  // their index so already-hashed entries are reused. Both are best-effort: a
169
176
  // missing or incompatible source just means a full, slower add.
package/src/tui/home.js CHANGED
@@ -42,17 +42,19 @@ export const PRIMARY_ACTIONS = [
42
42
  run: async () => (await import('../reporting.js')).buildUsageText()
43
43
  },
44
44
  { id: 'config', label: 'config', hint: 'provider, model, API key', kind: 'config' },
45
- { id: 'more', label: 'more…', hint: 'context · checkpoints · undo · mcp · update', kind: 'more' }
45
+ { id: 'more', label: 'more…', hint: 'sessions · model · approvals · tools', kind: 'more' }
46
46
  ];
47
47
 
48
48
  /**
49
49
  * The second page.
50
50
  *
51
- * These are real, and several of them are destructive, which is why they are
52
- * one level down rather than absent: hiding `unpair` would not make it safer,
53
- * it would make it undiscoverable.
51
+ * These are real local capabilities kept one level down so the first screen
52
+ * stays readable while the complete command surface remains discoverable.
54
53
  */
55
54
  export const MORE_ACTIONS = [
55
+ { id: 'sessions', label: 'sessions', category: 'workspace', hint: 'browse saved local sessions', kind: 'command', command: '/sessions' },
56
+ { id: 'model', label: 'model', category: 'workspace', hint: 'show the active provider and model', kind: 'command', command: '/model' },
57
+ { id: 'approvals', label: 'approvals', category: 'workspace', hint: 'review standing session approvals', kind: 'command', command: '/approvals' },
56
58
  {
57
59
  id: 'context', label: 'context', category: 'reports', hint: 'what fills the context window', kind: 'stay',
58
60
  run: async () => (await import('../reporting.js')).buildContextText()
@@ -69,14 +71,21 @@ export const MORE_ACTIONS = [
69
71
  command: '/undo'
70
72
  },
71
73
  { id: 'mcp', label: 'mcp', category: 'tools', hint: 'manage MCP servers', kind: 'mcp' },
74
+ { id: 'help', label: 'commands', category: 'tools', hint: 'show every slash command', kind: 'command', command: '/help' },
75
+ {
76
+ id: 'doctor', label: 'doctor', category: 'tools', hint: 'installation, runtime and update channel', kind: 'stay',
77
+ run: async () => {
78
+ const { diagnoseInstallation, formatDoctorReport } = await import('../autoUpdate.js');
79
+ return formatDoctorReport(await diagnoseInstallation());
80
+ }
81
+ },
72
82
  {
73
83
  id: 'update', label: 'update', category: 'tools', hint: 'update to the latest version', kind: 'takeover',
74
84
  run: async () => {
75
85
  const { runSelfUpdate } = await import('../autoUpdate.js');
76
86
  await runSelfUpdate(true);
77
87
  }
78
- },
79
-
88
+ }
80
89
  ];
81
90
 
82
91
  /** Items in the shape SelectList wants, for the `more…` page. */
package/src/version.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { fileURLToPath } from 'url';
3
4
 
4
5
  function getDir() {
5
6
  if (typeof __dirname !== 'undefined') {
@@ -10,7 +11,7 @@ function getDir() {
10
11
  if (metaUrl && typeof metaUrl === 'string') {
11
12
  const url = new URL(metaUrl);
12
13
  if (url.protocol === 'file:') {
13
- return path.dirname(url.pathname);
14
+ return path.dirname(fileURLToPath(url));
14
15
  }
15
16
  }
16
17
  } catch (e) {}
@@ -35,4 +36,3 @@ export function getPackageVersion() {
35
36
 
36
37
  return '1.3.14';
37
38
  }
38
-