cuttlely 0.0.0-stage → 1.0.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,13 @@
1
+ Copyright 2026 The Cuttlely Authors
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
package/README.md CHANGED
@@ -1,3 +1,50 @@
1
- # Temporary Holding Version
1
+ # cuttlely
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Start [Cuttlely](https://cuttlely.com) in Docker. Cuttlely is the open-source agent platform for applications.
4
+
5
+ The full server needs the Python agent runtime that ships in the Docker image, so this package does not install the server. `npx cuttlely` pulls `ghcr.io/cuttlely/cuttlely` and runs it.
6
+
7
+ Requires Node.js 20 or newer, and Docker.
8
+
9
+ ## Start
10
+
11
+ ```bash
12
+ npx cuttlely start
13
+ ```
14
+
15
+ That pulls `ghcr.io/cuttlely/cuttlely:1.0.0` (the same version as this package), keeps data in the Docker volume `cuttlely-data`, publishes port **43117**, waits until `http://127.0.0.1:43117/api/v1/ping` answers `pong`, prints `http://localhost:43117`, and opens it.
16
+
17
+ ```bash
18
+ npx cuttlely start --port 8080 --tag 1.0.0 --env-file .env --no-open
19
+ ```
20
+
21
+ | Flag | Meaning |
22
+ | ------------ | ------------------------------------------------------------------------ |
23
+ | `--port` | Host and container port. Default `43117`. |
24
+ | `--tag` | Image tag. Default is this package's version. `latest` is allowed. |
25
+ | `--env-file` | Passed through to `docker run`. The port and data paths above still win. |
26
+ | `--no-open` | Print the URL and skip the browser. |
27
+
28
+ A global install works the same way: `npm install -g cuttlely`, then `cuttlely start`.
29
+
30
+ If Docker is missing or not running, the command says so and links to the [install guide](https://github.com/cuttlely/cuttlely/blob/main/docs/how-to/docker.md).
31
+
32
+ ## Other commands
33
+
34
+ ```bash
35
+ cuttlely stop # stop the container; running answers get up to 35 seconds to finish
36
+ cuttlely status
37
+ cuttlely logs
38
+ cuttlely logs --follow
39
+ cuttlely upgrade # pull the image and start it again
40
+ cuttlely --help
41
+ cuttlely --version
42
+ ```
43
+
44
+ The container name is `cuttlely`. `start` leaves it running when it is already on the same image and port. `upgrade` always pulls and starts it again. The volume is `cuttlely-data`, mounted at `/var/cuttlely`.
45
+
46
+ Compose uses the volume name `cuttlely_cuttlely-data`. This launcher uses `cuttlely-data`.
47
+
48
+ The first account is created from the one-time setup link in `cuttlely logs`.
49
+
50
+ This package makes no network call of its own. Docker pulls the image, and the command waits on the local health check.
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../lib/cli.js'
3
+
4
+ try {
5
+ process.exitCode = await main(process.argv.slice(2))
6
+ } catch (error) {
7
+ console.error(error instanceof Error ? error.message : String(error))
8
+ process.exitCode = 1
9
+ }
package/lib/cli.js ADDED
@@ -0,0 +1,448 @@
1
+ // The full server needs the Python agent runtime, which ships in the Docker image.
2
+ // This package only talks to the local Docker daemon.
3
+ import { readFileSync, statSync } from 'node:fs'
4
+ import { spawn } from 'node:child_process'
5
+ import { dirname, join } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+ import { spawnDocker } from './docker.js'
8
+
9
+ const PKG = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json')
10
+
11
+ export const VERSION = JSON.parse(readFileSync(PKG, 'utf8')).version
12
+ export const IMAGE = 'ghcr.io/cuttlely/cuttlely'
13
+ export const DEFAULT_PORT = 43117
14
+ export const CONTAINER_NAME = 'cuttlely'
15
+ export const VOLUME_NAME = 'cuttlely-data'
16
+ export const DATA_DIR = '/var/cuttlely'
17
+ export const INSTALL_DOCS = 'https://github.com/cuttlely/cuttlely/blob/main/docs/how-to/docker.md'
18
+ export const HEALTH_TIMEOUT_MS = 6 * 60 * 1000
19
+
20
+ const TAG_RE = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/
21
+ const INSPECT_FORMAT = '{{.State.Status}}|{{.Config.Image}}|{{.Image}}|{{range $p, $b := .HostConfig.PortBindings}}{{$p}} {{end}}'
22
+
23
+ export class UsageError extends Error {}
24
+
25
+ export function helpText() {
26
+ return [
27
+ 'cuttlely runs Cuttlely in Docker.',
28
+ '',
29
+ 'Usage:',
30
+ ' cuttlely start [--port <n>] [--tag <tag>] [--env-file <path>] [--no-open]',
31
+ ' cuttlely stop',
32
+ ' cuttlely status',
33
+ ' cuttlely logs [--follow]',
34
+ ' cuttlely upgrade [--port <n>] [--tag <tag>] [--env-file <path>] [--no-open]',
35
+ ' cuttlely --help',
36
+ ' cuttlely --version',
37
+ '',
38
+ `The image is ${IMAGE}. The tag defaults to this package's version (${VERSION}).`,
39
+ `Data is kept in the Docker volume ${VOLUME_NAME}.`,
40
+ `The app listens on http://localhost:${DEFAULT_PORT} unless you set --port.`,
41
+ '',
42
+ 'Commands:',
43
+ ' start Pull the image and start Cuttlely. Opens the browser when it is ready.',
44
+ ' stop Stop the Cuttlely container. Running answers get up to 35 seconds to finish.',
45
+ ' status Show whether Cuttlely is running.',
46
+ ' logs Show container logs. --follow keeps streaming.',
47
+ ' upgrade Pull the image and start it again.',
48
+ '',
49
+ `Install guide: ${INSTALL_DOCS}`
50
+ ].join('\n')
51
+ }
52
+
53
+ function parsePort(value) {
54
+ if (!value || !/^[1-9]\d{0,4}$/.test(value) || Number(value) > 65535) {
55
+ throw new UsageError(`Port must be a number from 1 to 65535, got ${value || '(missing)'}.`)
56
+ }
57
+ return Number(value)
58
+ }
59
+
60
+ function parseTag(value) {
61
+ if (!value || !TAG_RE.test(value)) {
62
+ throw new UsageError(`Not a valid image tag: ${value || '(missing)'}. Example: 1.0.0 or latest.`)
63
+ }
64
+ return value
65
+ }
66
+
67
+ function takeValue(argv, index, flag) {
68
+ const inline = argv[index].indexOf('=')
69
+ if (inline !== -1) return { value: argv[index].slice(inline + 1), next: index }
70
+ const value = argv[index + 1]
71
+ if (!value || value.startsWith('-')) throw new UsageError(`${flag} needs a value. Run cuttlely --help.`)
72
+ return { value, next: index + 1 }
73
+ }
74
+
75
+ export function parseArgs(argv) {
76
+ const opts = {
77
+ command: null,
78
+ help: false,
79
+ version: false,
80
+ port: DEFAULT_PORT,
81
+ tag: VERSION,
82
+ envFile: null,
83
+ open: true,
84
+ follow: false
85
+ }
86
+ const words = []
87
+ for (let i = 0; i < argv.length; i++) {
88
+ const arg = argv[i]
89
+ if (arg === '--help' || arg === '-h') opts.help = true
90
+ else if (arg === '--version' || arg === '-V') opts.version = true
91
+ else if (arg === '--no-open') opts.open = false
92
+ else if (arg === '--follow' || arg === '-f') opts.follow = true
93
+ else if (arg === '--port' || arg === '-p' || arg.startsWith('--port=')) {
94
+ const taken = takeValue(argv, i, '--port')
95
+ opts.port = parsePort(taken.value)
96
+ i = taken.next
97
+ } else if (arg === '--tag' || arg.startsWith('--tag=')) {
98
+ const taken = takeValue(argv, i, '--tag')
99
+ opts.tag = parseTag(taken.value)
100
+ i = taken.next
101
+ } else if (arg === '--env-file' || arg.startsWith('--env-file=')) {
102
+ const taken = takeValue(argv, i, '--env-file')
103
+ opts.envFile = taken.value
104
+ i = taken.next
105
+ } else if (arg.startsWith('-')) throw new UsageError(`Unknown option ${arg}. Run cuttlely --help.`)
106
+ else words.push(arg)
107
+ }
108
+ if (words.length > 1) throw new UsageError(`Unknown argument ${words[1]}. Run cuttlely --help.`)
109
+ if (words.length === 1) {
110
+ if (!['start', 'stop', 'status', 'logs', 'upgrade', 'help'].includes(words[0])) {
111
+ throw new UsageError(`Unknown command ${words[0]}. Run cuttlely --help.`)
112
+ }
113
+ opts.command = words[0]
114
+ }
115
+ return opts
116
+ }
117
+
118
+ export function containerRunArgs({ image, port, envFile }) {
119
+ const args = [
120
+ 'run',
121
+ '-d',
122
+ '--name',
123
+ CONTAINER_NAME,
124
+ '--restart',
125
+ 'unless-stopped',
126
+ '--init',
127
+ '--stop-timeout',
128
+ '35',
129
+ '-p',
130
+ `${port}:${port}`,
131
+ '-v',
132
+ `${VOLUME_NAME}:${DATA_DIR}`,
133
+ '-e',
134
+ 'HOST=0.0.0.0',
135
+ '-e',
136
+ `PORT=${port}`,
137
+ '-e',
138
+ 'NODE_OPTIONS=--max-old-space-size=2048',
139
+ '-e',
140
+ `DATABASE_PATH=${DATA_DIR}/data`,
141
+ '-e',
142
+ `SECRETKEY_PATH=${DATA_DIR}/data`,
143
+ '-e',
144
+ `BLOB_STORAGE_PATH=${DATA_DIR}/storage`,
145
+ '-e',
146
+ `LOG_PATH=${DATA_DIR}/logs`,
147
+ '-e',
148
+ `CUTTLELY_DATA_DIR=${DATA_DIR}`,
149
+ '-e',
150
+ `CUTTLELY_HERMES_HOME=${DATA_DIR}/hermes`,
151
+ '-e',
152
+ `HERMES_HOME=${DATA_DIR}/hermes`,
153
+ '-e',
154
+ `CUTTLELY_ETL_DIR=${DATA_DIR}/etl`,
155
+ '-e',
156
+ 'CUTTLELY_INSTALL=docker',
157
+ '--health-cmd',
158
+ `curl -fsS "http://127.0.0.1:${port}/api/v1/ping"`,
159
+ '--health-interval',
160
+ '15s',
161
+ '--health-timeout',
162
+ '5s',
163
+ '--health-retries',
164
+ '20',
165
+ '--health-start-period',
166
+ '300s'
167
+ ]
168
+ if (envFile) args.push('--env-file', envFile)
169
+ args.push(image)
170
+ return args
171
+ }
172
+
173
+ export function publicUrl(port) {
174
+ return `http://localhost:${port}`
175
+ }
176
+
177
+ export function healthUrl(port) {
178
+ return `http://127.0.0.1:${port}/api/v1/ping`
179
+ }
180
+
181
+ function dockerMissingMessage() {
182
+ return [
183
+ 'Docker is not installed, or it is not on your PATH.',
184
+ '',
185
+ 'Cuttlely runs in Docker. Install Docker, start it, and run this command again.',
186
+ INSTALL_DOCS
187
+ ].join('\n')
188
+ }
189
+
190
+ function dockerDownMessage(detail) {
191
+ const lines = ['Docker is installed, but it is not running.', '', 'Start Docker and run this command again.', INSTALL_DOCS]
192
+ if (detail && detail.trim()) lines.push('', detail.trim())
193
+ return lines.join('\n')
194
+ }
195
+
196
+ function assertEnvFile(file) {
197
+ if (!file) return
198
+ let info
199
+ try {
200
+ info = statSync(file)
201
+ } catch {
202
+ throw new UsageError(`Env file not found: ${file}`)
203
+ }
204
+ if (!info.isFile()) throw new UsageError(`Env file is not a file: ${file}`)
205
+ }
206
+
207
+ async function ensureDocker(docker, err) {
208
+ const result = await docker(['info', '--format', '{{.ServerVersion}}'])
209
+ if (result.error && result.error.code === 'ENOENT') {
210
+ err(dockerMissingMessage())
211
+ return false
212
+ }
213
+ if (result.code !== 0 || result.error) {
214
+ err(dockerDownMessage(result.stderr || result.error?.message || ''))
215
+ return false
216
+ }
217
+ return true
218
+ }
219
+
220
+ async function inspectContainer(docker) {
221
+ const result = await docker(['inspect', CONTAINER_NAME, '--format', INSPECT_FORMAT])
222
+ if (result.code !== 0 || result.error) {
223
+ const text = `${result.stderr || ''}\n${result.stdout || ''}`
224
+ if (/No such object|no such container/i.test(text)) return null
225
+ return { error: (result.stderr || result.error?.message || 'Could not inspect the Cuttlely container.').trim() }
226
+ }
227
+ const [status, image, imageId, ports = ''] = result.stdout.trim().split('|')
228
+ const portMatch = /(\d+)\//.exec(ports)
229
+ return { status, image, imageId, port: portMatch ? Number(portMatch[1]) : null }
230
+ }
231
+
232
+ async function readImageId(docker, image) {
233
+ const result = await docker(['image', 'inspect', image, '--format', '{{.Id}}'])
234
+ if (result.code !== 0 || result.error) return ''
235
+ return result.stdout.trim()
236
+ }
237
+
238
+ async function waitForHealth({ url, fetchImpl, sleep, timeoutMs, now }) {
239
+ const deadline = now() + timeoutMs
240
+ for (;;) {
241
+ try {
242
+ const res = await fetchImpl(url, { redirect: 'manual' })
243
+ const body = typeof res.text === 'function' ? await res.text() : String(res.body ?? '')
244
+ if (res.ok && body.includes('pong')) return true
245
+ } catch {
246
+ // The server is still booting, or the port is not open yet.
247
+ }
248
+ if (now() >= deadline) return false
249
+ await sleep(1000)
250
+ }
251
+ }
252
+
253
+ async function reportReady(ctx) {
254
+ const url = publicUrl(ctx.opts.port)
255
+ const ping = healthUrl(ctx.opts.port)
256
+ ctx.out(`Waiting for Cuttlely at ${ping}`)
257
+ const ready = await waitForHealth({
258
+ url: ping,
259
+ fetchImpl: ctx.fetchImpl,
260
+ sleep: ctx.sleep,
261
+ timeoutMs: ctx.timeoutMs,
262
+ now: ctx.now
263
+ })
264
+ if (!ready) {
265
+ ctx.err(`Cuttlely is still starting. Open ${url} when it is ready.`)
266
+ ctx.err('Logs: cuttlely logs')
267
+ ctx.err(INSTALL_DOCS)
268
+ return 1
269
+ }
270
+ ctx.out(`Cuttlely is ready. Open ${url}`)
271
+ if (ctx.opts.open) ctx.open(url)
272
+ return 0
273
+ }
274
+
275
+ async function removeContainer(docker) {
276
+ await docker(['stop', '-t', '35', CONTAINER_NAME])
277
+ return docker(['rm', CONTAINER_NAME])
278
+ }
279
+
280
+ async function launch(ctx, { restart }) {
281
+ if (!(await ensureDocker(ctx.docker, ctx.err))) return 1
282
+ assertEnvFile(ctx.opts.envFile)
283
+ const image = `${IMAGE}:${ctx.opts.tag}`
284
+ ctx.out(`Pulling ${image}`)
285
+ const pulled = await ctx.docker(['pull', image], { stdio: 'inherit' })
286
+ if (pulled.code !== 0 || pulled.error) {
287
+ const detail = (pulled.stderr || pulled.error?.message || '').trim()
288
+ ctx.err([`Could not pull ${image}.`, 'Check the tag, and see the install guide.', INSTALL_DOCS, detail].filter(Boolean).join('\n'))
289
+ return 1
290
+ }
291
+ const imageId = await readImageId(ctx.docker, image)
292
+ const current = await inspectContainer(ctx.docker)
293
+ if (current && current.error) {
294
+ ctx.err(current.error)
295
+ return 1
296
+ }
297
+ const same = current && current.image === image && current.imageId === imageId && current.port === ctx.opts.port && !ctx.opts.envFile
298
+ if (current && same && current.status === 'running' && !restart) {
299
+ ctx.out('Cuttlely is already running.')
300
+ return reportReady(ctx)
301
+ }
302
+ if (current && same && !restart) {
303
+ const started = await ctx.docker(['start', CONTAINER_NAME])
304
+ if (started.code !== 0 || started.error) {
305
+ ctx.err((started.stderr || started.error?.message || 'Could not start the Cuttlely container.').trim())
306
+ return 1
307
+ }
308
+ return reportReady(ctx)
309
+ }
310
+ if (current) {
311
+ const removed = await removeContainer(ctx.docker)
312
+ if (removed.code !== 0 || removed.error) {
313
+ ctx.err((removed.stderr || removed.error?.message || 'Could not replace the Cuttlely container.').trim())
314
+ return 1
315
+ }
316
+ }
317
+ const run = await ctx.docker(containerRunArgs({ image, port: ctx.opts.port, envFile: ctx.opts.envFile }))
318
+ if (run.code !== 0 || run.error) {
319
+ const detail = (run.stderr || run.error?.message || 'Could not start Cuttlely.').trim()
320
+ const hint = /port is already allocated|address already in use/i.test(detail)
321
+ ? `Port ${ctx.opts.port} is in use. Stop what is using it, or run cuttlely start --port <port>.`
322
+ : ''
323
+ ctx.err([detail, hint].filter(Boolean).join('\n'))
324
+ return 1
325
+ }
326
+ return reportReady(ctx)
327
+ }
328
+
329
+ async function stop(ctx) {
330
+ if (!(await ensureDocker(ctx.docker, ctx.err))) return 1
331
+ const current = await inspectContainer(ctx.docker)
332
+ if (current && current.error) {
333
+ ctx.err(current.error)
334
+ return 1
335
+ }
336
+ if (!current || !['running', 'restarting', 'paused'].includes(current.status)) {
337
+ ctx.out('Cuttlely is not running.')
338
+ return 0
339
+ }
340
+ const result = await ctx.docker(['stop', '-t', '35', CONTAINER_NAME])
341
+ if (result.code !== 0 || result.error) {
342
+ ctx.err((result.stderr || result.error?.message || 'Could not stop Cuttlely.').trim())
343
+ return 1
344
+ }
345
+ ctx.out('Stopped Cuttlely.')
346
+ return 0
347
+ }
348
+
349
+ async function status(ctx) {
350
+ if (!(await ensureDocker(ctx.docker, ctx.err))) return 1
351
+ const current = await inspectContainer(ctx.docker)
352
+ if (current && current.error) {
353
+ ctx.err(current.error)
354
+ return 1
355
+ }
356
+ if (!current) {
357
+ ctx.out('Cuttlely is not running.')
358
+ ctx.out('Start it with: cuttlely start')
359
+ return 0
360
+ }
361
+ ctx.out(`name: ${CONTAINER_NAME}`)
362
+ ctx.out(`status: ${current.status}`)
363
+ ctx.out(`image: ${current.image}`)
364
+ ctx.out(`volume: ${VOLUME_NAME}`)
365
+ if (current.status === 'running' && current.port) ctx.out(`url: ${publicUrl(current.port)}`)
366
+ return 0
367
+ }
368
+
369
+ async function logs(ctx) {
370
+ if (!(await ensureDocker(ctx.docker, ctx.err))) return 1
371
+ const current = await inspectContainer(ctx.docker)
372
+ if (current && current.error) {
373
+ ctx.err(current.error)
374
+ return 1
375
+ }
376
+ if (!current) {
377
+ ctx.err('No Cuttlely container yet. Start it with: cuttlely start')
378
+ return 1
379
+ }
380
+ const args = ctx.opts.follow ? ['logs', '-f', CONTAINER_NAME] : ['logs', CONTAINER_NAME]
381
+ const result = await ctx.docker(args, ctx.opts.follow ? { stdio: 'inherit' } : {})
382
+ if (!ctx.opts.follow) {
383
+ if (result.stdout) ctx.out(result.stdout.replace(/\s*$/, ''))
384
+ if (result.stderr && result.stderr.trim()) ctx.err(result.stderr.replace(/\s*$/, ''))
385
+ }
386
+ return result.code === 0 && !result.error ? 0 : 1
387
+ }
388
+
389
+ function openBrowser(url) {
390
+ const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'rundll32' : 'xdg-open'
391
+ const args = process.platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url]
392
+ try {
393
+ const child = spawn(command, args, { stdio: 'ignore', detached: true })
394
+ child.on('error', () => {})
395
+ child.unref()
396
+ } catch {
397
+ // The URL is already printed.
398
+ }
399
+ }
400
+
401
+ export async function main(argv, io = {}) {
402
+ const out = io.out || ((line) => console.info(line))
403
+ const err = io.err || ((line) => console.error(line))
404
+ let opts
405
+ try {
406
+ opts = parseArgs(argv)
407
+ } catch (error) {
408
+ if (error instanceof UsageError) {
409
+ err(error.message)
410
+ return 1
411
+ }
412
+ throw error
413
+ }
414
+ if (opts.version) {
415
+ out(VERSION)
416
+ return 0
417
+ }
418
+ if (opts.help || !opts.command || opts.command === 'help') {
419
+ out(helpText())
420
+ return 0
421
+ }
422
+ const ctx = {
423
+ opts,
424
+ out,
425
+ err,
426
+ docker: io.docker || spawnDocker,
427
+ fetchImpl: io.fetch || globalThis.fetch.bind(globalThis),
428
+ sleep: io.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
429
+ open: io.open || openBrowser,
430
+ now: io.now || Date.now,
431
+ timeoutMs: io.healthTimeoutMs ?? HEALTH_TIMEOUT_MS
432
+ }
433
+ try {
434
+ if (opts.command === 'start') return await launch(ctx, { restart: false })
435
+ if (opts.command === 'upgrade') return await launch(ctx, { restart: true })
436
+ if (opts.command === 'stop') return await stop(ctx)
437
+ if (opts.command === 'status') return await status(ctx)
438
+ if (opts.command === 'logs') return await logs(ctx)
439
+ } catch (error) {
440
+ if (error instanceof UsageError) {
441
+ err(error.message)
442
+ return 1
443
+ }
444
+ throw error
445
+ }
446
+ err(`Unknown command ${opts.command}. Run cuttlely --help.`)
447
+ return 1
448
+ }
package/lib/docker.js ADDED
@@ -0,0 +1,37 @@
1
+ import { spawn } from 'node:child_process'
2
+
3
+ // Talks only to the local Docker CLI. Callers pass an argv array, never a shell string.
4
+ export function spawnDocker(args, { stdio = 'pipe' } = {}) {
5
+ return new Promise((resolve) => {
6
+ let settled = false
7
+ const finish = (result) => {
8
+ if (settled) return
9
+ settled = true
10
+ resolve(result)
11
+ }
12
+ let child
13
+ try {
14
+ child = spawn('docker', args, {
15
+ stdio: stdio === 'inherit' ? 'inherit' : ['ignore', 'pipe', 'pipe'],
16
+ windowsHide: true
17
+ })
18
+ } catch (error) {
19
+ finish({ code: 127, stdout: '', stderr: '', error })
20
+ return
21
+ }
22
+ const stdout = []
23
+ const stderr = []
24
+ if (stdio !== 'inherit') {
25
+ child.stdout.setEncoding('utf8')
26
+ child.stderr.setEncoding('utf8')
27
+ child.stdout.on('data', (chunk) => stdout.push(chunk))
28
+ child.stderr.on('data', (chunk) => stderr.push(chunk))
29
+ }
30
+ child.on('error', (error) => {
31
+ finish({ code: 127, stdout: stdout.join(''), stderr: stderr.join(''), error })
32
+ })
33
+ child.on('close', (code) => {
34
+ finish({ code: code ?? 1, stdout: stdout.join(''), stderr: stderr.join(''), error: null })
35
+ })
36
+ })
37
+ }
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
- "name": "cuttlely",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "cuttlely",
3
+ "version": "1.0.0",
4
+ "description": "Start Cuttlely in Docker. The open-source agent platform for applications.",
5
+ "type": "module",
6
+ "bin": {
7
+ "cuttlely": "./bin/cuttlely.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "lib",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "license": "Apache-2.0",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/cuttlely/cuttlely.git",
19
+ "directory": "packages/cli"
20
+ },
21
+ "homepage": "https://cuttlely.com",
22
+ "keywords": [
23
+ "cuttlely",
24
+ "agents",
25
+ "cli",
26
+ "docker"
27
+ ],
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "scripts": {
35
+ "test": "node --test test/cli.test.js"
36
+ }
37
+ }