@skillcdn/console 0.1.7 → 0.1.9

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.
@@ -0,0 +1,90 @@
1
+ # Sizing and cost: three sizes, and what each costs
2
+
3
+ How the organization's size becomes a size of the deployment, what each size is made of, how its monthly cost is estimated before anything is made, and what changes when the organization grows. Every price here is a snapshot that illustrates the shape, marked with the month it was read; the run reads today's prices where it can, shows those, and says which lines are still the snapshot.
4
+
5
+ ## The sizes
6
+
7
+ The question is how many people will use the console within a year; an answer stands for the run and is written in the record. Growing later is a change of numbers (below).
8
+
9
+ | Size | For | The `api` service | The `worker` | The database | What else |
10
+ |---|---|---|---|---|---|
11
+ | `small` | Up to 10 people | One task, 0.25 vCPU, 0.5 GB, carrying the worker's work (`WORKER_IN_PROCESS=true`) | In the `api` task | One instance, the smallest class with 1 GB of memory (`db.t4g.micro`), 20 GB growing to 100, one zone | A bucket, the parameter store, one public address for the task's egress |
12
+ | `medium` | Up to 30 people | One task, 0.5 vCPU, 1 GB, carrying the worker's work | In the `api` task | One instance with 2 GB (`db.t4g.small`), 20 GB growing to 100, one zone | The same |
13
+ | `large` | Up to 100 people | Two tasks, 0.5 vCPU, 1 GB each | A service of its own, one task, 0.25 vCPU, 0.5 GB | One instance with 4 GB (`db.t4g.medium`), 20 GB growing to 200; in two zones when the organization wants the database to survive a zone | The same, with three public addresses |
14
+
15
+ The board is a light load: a page, a feed held open per browser tab with a heartbeat every 25 seconds, a command now and then from each agent. What grows with people is the number of open feeds (memory and connections) and the database's working set, not the CPU. These sizes leave room; the delivery says how to read the gauges (below) and the record says which size was chosen.
16
+
17
+ ## What is billed
18
+
19
+ | Item | How it is billed | Illustrative price, N. Virginia (`us-east-1`), read 2026-10 |
20
+ |---|---|---|
21
+ | Fargate tasks | Per vCPU-hour and per GB-hour of each task, per second, a minute at least; x86 by default, ARM about a fifth cheaper when the image is built for it | x86 $0.04048 per vCPU-hour, $0.004445 per GB-hour; ARM $0.03238 and $0.00356 |
22
+ | Public IPv4 address, one per task | Per hour | $0.005 per hour, about $3.65 a month |
23
+ | RDS PostgreSQL instance | Per hour by class; two zones doubles it | `db.t4g.micro` $0.016 per hour, `db.t4g.small` $0.032 per hour, `db.t4g.medium` about twice the small |
24
+ | RDS storage, `gp3` | Per GB-month of what is allocated; backups up to the instance's size are free | $0.115 per GB-month |
25
+ | S3 | Per GB-month and per request; a board's files are small | $0.023 per GB-month |
26
+ | ECR | Per GB-month of images kept | $0.10 per GB-month |
27
+ | CloudWatch Logs | Per GB ingested and per GB-month kept, 30 days here | $0.50 per GB ingested, $0.03 per GB-month |
28
+ | SSM Parameter Store | Standard parameters free; the certificate bundle is an advanced parameter | $0.05 per advanced parameter a month |
29
+ | Cloudflare | The Free plan: DNS, the proxy, the tunnel, one rate limiting rule, certificates | $0 |
30
+ | A domain | Per year, at cost through the Registrar | About $10 a year for a `.com` |
31
+ | GitHub Actions | Minutes of the deployment repository's builds; a build takes about ten | Within the minutes a plan includes |
32
+
33
+ Other regions are dearer by a tenth to a third for the same items (Tokyo's ARM task rates were about a quarter above N. Virginia's, read 2026-10); Seoul's rates are read from the price list in the run. A new AWS account made after 2025-07-15 starts with credits ($100 at sign-up, up to $200 with the first activities), which the paid plan spends on these bills over the first months; the free plan ends after six months and is not for a console that stays.
34
+
35
+ ## The estimate, size by size
36
+
37
+ Illustrative, from the prices above, x86 tasks, one zone, 730 hours a month, read 2026-10. The run recomputes each line with the prices of the day and the region, and shows the chosen size with the next one beside it.
38
+
39
+ | Size | Tasks | Addresses | Database | Storage, logs, images | About |
40
+ |---|---|---|---|---|---|
41
+ | `small` | $9 | $4 | $12 | $3 | $28 a month |
42
+ | `medium` | $18 | $4 | $23 | $4 | $49 a month |
43
+ | `large` | $45 | $11 | $47 (one zone) or $95 (two) | $6 | $109 a month, $157 in two zones |
44
+
45
+ A domain is on top, per year. Cloudflare costs nothing on the Free plan.
46
+
47
+ ## Reading today's prices
48
+
49
+ The two lines that matter, the tasks and the database, come from the Price List API, which answers from `us-east-1` whatever the region priced (the deployer's policy allows `pricing:GetProducts`); the small lines (the address, storage, logs, images) from the pricing pages, or the snapshot with the region's premium stated. The database's instance, per hour:
50
+
51
+ ```sh
52
+ aws pricing get-products --region us-east-1 --service-code AmazonRDS \
53
+ --filters Type=TERM_MATCH,Field=instanceType,Value=<db.t4g.micro | db.t4g.small | db.t4g.medium> \
54
+ Type=TERM_MATCH,Field=databaseEngine,Value=PostgreSQL \
55
+ Type=TERM_MATCH,Field=deploymentOption,Value=<Single-AZ | Multi-AZ> \
56
+ Type=TERM_MATCH,Field=regionCode,Value=<region> \
57
+ --query 'PriceList[0]' --output text | jq -r '.terms.OnDemand[] | .priceDimensions[] | .pricePerUnit.USD'
58
+ ```
59
+
60
+ The tasks, per vCPU-hour and per GB-hour, by the usage type of the region (`APN2` for Seoul, `APN1` for Tokyo, `EUC1` for Frankfurt, `EUW1` for Ireland, `USE2` for Ohio, no prefix for N. Virginia; `ARM-` before `vCPU` and `GB` for ARM tasks):
61
+
62
+ ```sh
63
+ for usage in "<prefix>-Fargate-vCPU-Hours:perCPU" "<prefix>-Fargate-GB-Hours"; do
64
+ aws pricing get-products --region us-east-1 --service-code AmazonECS \
65
+ --filters Type=TERM_MATCH,Field=usagetype,Value="$usage" Type=TERM_MATCH,Field=regionCode,Value=<region> \
66
+ --query 'PriceList[0]' --output text | jq -r '.terms.OnDemand[] | .priceDimensions[] | .pricePerUnit.USD'
67
+ done
68
+ ```
69
+
70
+ The pages, each with the region selected, for the rest: the public IPv4 charge on the EC2 pricing page (`aws.amazon.com/ec2/pricing/on-demand`, "IPv4 address"), S3 (`aws.amazon.com/s3/pricing`), ECR, CloudWatch; or the AWS Pricing Calculator. A lookup that fails is said so in the plan, and that line is the snapshot with the premium of "What is billed" applied, named as an estimate. State in the plan where each number came from and the month.
71
+
72
+ ## The estimate message
73
+
74
+ Part of the plan, in plain words, one line per item, the chosen size first:
75
+
76
+ > For <n> people I would set it up as **<size>**: <what it is made of, in words>. That is about **<total> a month** in <region> (<tasks>, <database>, <addresses>, <the rest>; <which lines are today's prices and which the snapshot>)<, plus the domain at <price> a year when one is bought>. The next size up, **<size>**, would be about <total> a month and is a change of numbers later, not a new deployment. A budget alert at <total × 1.5> will email <address> when the month's bill runs past it, or is forecast to.
77
+
78
+ ## Options the plan offers
79
+
80
+ - **The database as serverless capacity.** Aurora Serverless v2 for PostgreSQL scales its capacity with load, down to nothing when idle (0 ACU with automatic pause, since 2024-11). The console holds a listening connection open and sweeps every fifteen minutes, so it never idles and the database never pauses: the floor is 0.5 ACU, about $44 a month at $0.12 per ACU-hour (read 2026-10), more than the `small` and `medium` instances. Offer it when the organization wants capacity that follows load without resizing, and say the floor.
81
+ - **The bucket on Cloudflare R2** instead of S3: the same S3 API, `S3_ENDPOINT=https://<account id>.r2.cloudflarestorage.com`, `S3_REGION=auto`, path style; no egress charges. Its keys are made in the Cloudflare dashboard (R2, Manage API tokens, Object Read & Write on the one bucket), saved in the secrets file as `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY`, stored as parameters and carried by the `api` task as secrets, with no `S3_CREDENTIALS` line: the task's role cannot reach R2. Offer it when the organization keeps its files at Cloudflare already; S3 stays the default because the task's own role reaches it with no key to keep, and the budget and the audit are then in one account.
82
+ - **A load balancer instead of the tunnel.** ECS Express Mode creates an Application Load Balancer with HTTPS on an AWS domain in one command, at about $16 to $23 a month for the balancer (read 2026-10); the custom domain is a manual step on the balancer, and Cloudflare then proxies to it. Offer it only when the organization does not want its traffic through the tunnel; the tunnel is the default because it costs nothing, exposes nothing, and brings the client's address to the console cleanly.
83
+ - **ARM tasks** when the organization has ARM runners for its builds (building for ARM on an x86 runner takes many times longer): a fifth off the task line.
84
+
85
+ ## Growing
86
+
87
+ - **More people:** change the task sizes and counts in `deploy/api.task.json` and `deploy/worker.task.json` of the deployment repository and push; the services roll. At `large`, the `worker` becomes a service of its own and `WORKER_IN_PROCESS` is dropped from the `api` task; `deploy.yml` deploys both.
88
+ - **A bigger database:** `aws rds modify-db-instance --db-instance-identifier <name> --db-instance-class <class> --apply-immediately` (a few minutes of unavailability; do it when nobody works) and, for two zones, `--multi-az`. Storage grows on its own up to the maximum set.
89
+ - **Reading the gauges:** the ECS service's CPU and memory in CloudWatch (`AWS/ECS`, by cluster and service), the database's `FreeableMemory` and `DatabaseConnections` (`AWS/RDS`). Memory above 70% or connections near the instance's limit (87 on a `t4g.micro`) says the next size is due.
90
+ - **Fewer people, or a trial that ended:** the opposite; or `aws ecs update-service --desired-count 0` for both services and `aws rds stop-db-instance` (seven days at most, then it starts itself) to pause the deployment at the cost of storage alone.
@@ -0,0 +1,35 @@
1
+ // Writes the access key of a downloaded IAM key file into a profile of the AWS CLI's own
2
+ // credentials file, and deletes the key file. Neither value touches a command line, a message
3
+ // or the standard output. Usage: node import-aws-key.mjs <key file> <profile>
4
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { join } from "node:path";
7
+ import process from "node:process";
8
+
9
+ const [file, profile = "console"] = process.argv.slice(2);
10
+ if (file === undefined) {
11
+ process.stderr.write("usage: node import-aws-key.mjs <key file> <profile>\n");
12
+ process.exit(2);
13
+ }
14
+ const [header, row] = readFileSync(file, "utf8").trim().split(/\r?\n/);
15
+ const names = (header ?? "").split(",").map((name) => name.trim().toLowerCase());
16
+ const values = (row ?? "").split(",").map((value) => value.trim());
17
+ const id = values[names.findIndex((name) => name.includes("access key id"))];
18
+ const secret = values[names.findIndex((name) => name.includes("secret access key"))];
19
+ if (id === undefined || secret === undefined || id.length === 0 || secret.length === 0) {
20
+ process.stderr.write("the file does not look like a downloaded access key (.csv)\n");
21
+ process.exit(1);
22
+ }
23
+ const directory = join(homedir(), ".aws");
24
+ mkdirSync(directory, { recursive: true });
25
+ const credentials = join(directory, "credentials");
26
+ const before = existsSync(credentials)
27
+ ? readFileSync(credentials, "utf8").replace(new RegExp(`\\[${profile}\\][^[]*`), "")
28
+ : "";
29
+ writeFileSync(
30
+ credentials,
31
+ `${before.trimEnd()}\n\n[${profile}]\naws_access_key_id = ${id}\naws_secret_access_key = ${secret}\n`.trimStart(),
32
+ { mode: 0o600 },
33
+ );
34
+ unlinkSync(file);
35
+ process.stdout.write(`profile ${profile} written; ${file} deleted\n`);
@@ -0,0 +1,100 @@
1
+ // The secrets file of a run (the shape of assets/secrets.example.json), kept outside any
2
+ // repository, and the parameter store: values go in from standard input or from a command's
3
+ // answer, and out into parameters, without ever passing through a command line or the output.
4
+ //
5
+ // node store-secrets.mjs set <file> <NAME> the value is standard input
6
+ // node store-secrets.mjs derive <file> <NAME> <template> {{OTHER}} in the template is replaced
7
+ // node store-secrets.mjs render <file> <in> <out> a JSON file with {{NAME}} placeholders filled
8
+ // node store-secrets.mjs store <file> <prefix> every value as a SecureString parameter <prefix>/<NAME>
9
+ import { execFileSync } from "node:child_process";
10
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+ import process from "node:process";
14
+
15
+ const [mode, file, ...rest] = process.argv.slice(2);
16
+ const usage = () => {
17
+ process.stderr.write("usage: node store-secrets.mjs set|derive|render|store <file> ...\n");
18
+ process.exit(2);
19
+ };
20
+ if (mode === undefined || file === undefined) {
21
+ usage();
22
+ }
23
+
24
+ const read = () => (existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : {});
25
+ const write = (data) => writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
26
+ const fill = (data, text) =>
27
+ text.replace(/\{\{([A-Z0-9_]+)\}\}/g, (_, name) => {
28
+ if (typeof data[name] !== "string") {
29
+ throw new Error(`${name} is not in the secrets file`);
30
+ }
31
+ return data[name];
32
+ });
33
+ /** Runs the AWS CLI with a request body in a temporary file, so that nothing is on the command line. */
34
+ const aws = (args, body) => {
35
+ const directory = mkdtempSync(join(tmpdir(), "console-"));
36
+ try {
37
+ const request = join(directory, "request.json");
38
+ writeFileSync(request, JSON.stringify(body), { mode: 0o600 });
39
+ return execFileSync("aws", [...args, "--cli-input-json", `file://${request}`], {
40
+ encoding: "utf8",
41
+ stdio: ["ignore", "pipe", "inherit"],
42
+ });
43
+ } finally {
44
+ rmSync(directory, { recursive: true, force: true });
45
+ }
46
+ };
47
+
48
+ switch (mode) {
49
+ case "set": {
50
+ const [name] = rest;
51
+ if (name === undefined) {
52
+ usage();
53
+ }
54
+ const value = readFileSync(0, "utf8").replace(/\r?\n$/, "");
55
+ write({ ...read(), [name]: value });
56
+ break;
57
+ }
58
+ case "derive": {
59
+ const [name, template] = rest;
60
+ if (name === undefined || template === undefined) {
61
+ usage();
62
+ }
63
+ const data = read();
64
+ write({ ...data, [name]: fill(data, template) });
65
+ break;
66
+ }
67
+ case "render": {
68
+ const [input, output] = rest;
69
+ if (input === undefined || output === undefined) {
70
+ usage();
71
+ }
72
+ writeFileSync(output, fill(read(), readFileSync(input, "utf8")), { mode: 0o600 });
73
+ break;
74
+ }
75
+ case "store": {
76
+ const [prefix] = rest;
77
+ if (prefix === undefined) {
78
+ usage();
79
+ }
80
+ const data = read();
81
+ for (const [name, value] of Object.entries(data)) {
82
+ // The password lives inside the URL; a value the person deferred is not a parameter.
83
+ if (name === "_" || name === "DATABASE_PASSWORD" || value === "later") {
84
+ continue;
85
+ }
86
+ const tier = Buffer.byteLength(value, "utf8") > 4000 ? "Advanced" : "Standard";
87
+ aws(["ssm", "put-parameter"], {
88
+ Name: `${prefix}/${name}`,
89
+ Type: "SecureString",
90
+ Value: value,
91
+ Tier: tier,
92
+ Overwrite: true,
93
+ });
94
+ process.stdout.write(`${prefix}/${name} stored (${tier})\n`);
95
+ }
96
+ break;
97
+ }
98
+ default:
99
+ usage();
100
+ }
@@ -1,21 +1,29 @@
1
1
  ---
2
2
  name: working-the-board
3
- description: "Works a task from the board of a SkillCDN Console with the console command, as the person whose token the command holds: reads the task, takes it, reports at the milestones of the work, asks on the board when a person must decide and waits for the answer, hands in what was made as a link or a file, writes down what the next agent needs on a page of the project, and finishes. Use when an agent is told to take, work or finish a task on the console, or to report, ask, hand in or write a page there; any agent with a shell where the command is signed in. To build a console or a script against the console's API, use building-a-console."
3
+ description: "Works a task from the board of a SkillCDN Console with the console command, as the person whose token the command holds: reads the task, takes it, reports at the milestones of the work, asks on the board when a person must decide and waits for the answer, hands in what was made as a link or a file, writes down what the next agent needs on a page of the project, and finishes. Use when an agent is told to take, work or finish a task on the console, or to report, ask, hand in or write a page there; any agent with a shell where the command is signed in. To build a console or a script against the console's API, use building-a-console; to deploy a console for an organization, use deploying-a-console."
4
4
  license: MIT
5
5
  compatibility: Needs a shell with the console command on the PATH (npm install -g @skillcdn/console, Node.js 24 or newer), signed in by a person. Nothing else; the command needs no key of its own and the agent never sees a token.
6
6
  metadata:
7
7
  author: skillcdn
8
- version: "1.0"
8
+ version: "1.1"
9
9
  skillcdn:
10
10
  translations:
11
11
  ko:
12
12
  title: 콘솔 보드에서 일하기
13
- description: "SkillCDN 콘솔의 보드에 있는 작업을 console 명령으로 수행합니다. 명령에 로그인한 사람의 자격으로, 작업을 읽고 맡고, 진행의 이정표마다 보고하고, 사람이 정해야 할 일은 보드에 물어 답을 기다리고, 만든 것을 링크나 파일로 제출하고, 다음 에이전트에게 필요한 것을 프로젝트 페이지에 적고, 마칩니다. 에이전트에게 콘솔의 작업을 맡거나 끝내라고, 또는 보고·질문·제출·페이지 쓰기를 하라고 할 때 쓰세요. 콘솔이나 API 클라이언트를 만들 때는 building-a-console을 쓰세요."
13
+ description: "SkillCDN 콘솔의 보드에 있는 작업을 console 명령으로 수행합니다. 명령에 로그인한 사람의 자격으로, 작업을 읽고 맡고, 진행의 이정표마다 보고하고, 사람이 정해야 할 일은 보드에 물어 답을 기다리고, 만든 것을 링크나 파일로 제출하고, 다음 에이전트에게 필요한 것을 프로젝트 페이지에 적고, 마칩니다. 에이전트에게 콘솔의 작업을 맡거나 끝내라고, 또는 보고·질문·제출·페이지 쓰기를 하라고 할 때 쓰세요. 콘솔이나 API 클라이언트를 만들 때는 building-a-console을, 조직의 콘솔을 배포할 때는 deploying-a-console을 쓰세요."
14
14
  ---
15
15
  # Working a task on the board
16
16
 
17
17
  A task of the board goes in, named by its number; a run comes out: the task taken, the work reported at its milestones, a decision raised and waited for where a person had to choose, what was made handed in, what was learned written down for the next agent, and the run finished. The board is a SkillCDN Console ([architecture](/docs/architecture.md)): projects of tasks, the agents at work on them, and the decisions that wait for a person. The agent acts as the person who signed the command in, on that person's standing and no more.
18
18
 
19
+ ## How the person is involved
20
+
21
+ - **Questions:** only which task, when the request names none: "Which task should I take? Its number, or say that any ready one may be." Nothing else is asked in chat: what the task leaves open is asked on the board, where its people answer.
22
+ - **Checkpoints:** on the board, as decisions (phase 4), which a person answers from wherever they are; the reports say what was done at each milestone, and need no answer.
23
+ - **Go-ahead:** the task's body is the go-ahead. A task that says what the agent may decide alone is followed within that; everything else that would change scope, cost or a trade-off is a decision.
24
+ - **Changes mid-run:** a person changes the task or answers a decision on the board; the run takes it from there and says so in its next report. A report is never rewritten.
25
+ - **Language:** reports, questions, pages and summaries are in the language the task is written in, so that the people of the board read them as they wrote.
26
+
19
27
  ## Requirements
20
28
 
21
29
  | Need | How | When missing |