YouBothAgent▾
You — Business rules and flows you own. Read these yourself.
Both — Know the idea; your agent follows the details.
Agent — Conventions and references your agent follows. Look up as needed.
CLI Reference▾
AkanJS Reference▾
akanjs/common
akanjs/common holds small helpers that depend on no platform. The same import works in a page, a store, a service and a CLI script.import { Logger, sleep, isEmail } from "akanjs/common";On this page
ExportDescription
Writes leveled log lines and hands them to the sinks you register.
Waits the given number of milliseconds.
Changes the case of the first character only.
Adds dashes to a Korean phone number and checks the dashed form.
Checks that a string looks like an email address.
Calls a REST API that is not an Akan server.
Picks one or several random items from a list.
More in akanjs/common
ExportDescription
clamp
Keeps a number between
min and max.formatNumber
Adds thousands separators to a number string and keeps the decimals as written.
isValidDate
Tells whether a
YYYY-MM-DD string, Date or Dayjs parses, though 2024-02-30 still passes.isDayjs
Tells whether a value is a
Dayjs.splitVersionmergeVersion
Splits "1.2.3" into major, minor and patch, and joins them back.
objectifyplainFieldsOf
Copy data fields without methods, and only
plainFieldsOf keeps a model's Date fields.deepObjectify
Makes a deep plain copy, JSON-ready when you pass
serializable or convertDate.decodeJwtPayload
Reads a JWT's payload without checking its signature, so never trust it for access.
isThenable
Tells whether a value can be awaited.
interpolateTranslation
Fills
{name} placeholders and leaves one whose value is missing as written.The same import also carries route-convention helpers and wire contracts that the framework uses itself. App code rarely needs them.
Logger
Akan's leveled logger. A service already has one as
this.logger, named after its class, and an adapt() adapter has one named after its key. Anywhere else, create new Logger("Name") or call the static methods.A script that logs through an instance, a static call, a structured record and a sink:
apps/myapp/script/syncInvoices.ts
- Six levels, lowest first:
trace,verbose,debug,info,warn,error. A line prints at or above the console level, anderrorlines go to stderr. - The second argument is the context.
logger.warn("retrying charge", "stripe")prints[stripe]before the message. - Secret-looking keys are masked. An
attrskey containing a word such as password, token, secret, cookie or api key reads[redacted]before any sink sees it. - Give every sink a floor. A sink without
minLeveltakes every level down toAKAN_LOG_FILE_LEVEL(trace), so eachverbosecall gets rendered.
Methods
APIDescription
logger.info(msg, context?)
One method per level:
trace, verbose, debug, info, warn, error.Logger.info(msg, context?, name?)
The same methods as statics, with
name defaulting to App.Logger.setLevel(level)
Changes the console level while the process runs.
Logger.shouldLog(level)
Tells whether a line at that level would go anywhere, before you build a costly message.
Logger.addSink(sink, { minLevel })
Passes each record at or above
minLevel to your function and returns its remover.Logger.removeSink(sink)
Stops passing records to that sink.
Logger.emit({ level, name, message, attrs })
Writes one record with
key=value attributes after the message.Environment variables
AKAN_PUBLIC_LOG_LEVELLogLeveldefault info
The console level, below which lines are not printed.
AKAN_LOG_STDOUT_LEVELLogLeveldefault AKAN_PUBLIC_LOG_LEVEL
The level the container's stdout carries, and it overrides
AKAN_PUBLIC_LOG_LEVEL when set.AKAN_LOG_FILE_LEVELLogLeveldefault trace
The floor for a sink that sets no
minLevel.

Never call
.log(). It is deprecated and writes at info, so it looks like its own level but is not; lint rejects it. AKAN_PUBLIC_LOG_LEVEL=log likewise means info.sleep
sleep(ms) returns a Promise that resolves after ms milliseconds. It is used for polling, retry waits, tests and the CLI's cloud sign-in loop.The shared file helper polls an upload until it leaves
uploading:libs/shared/webkit/addFileUntilActive.ts
- It does not block. Other work keeps running during the wait; only the function that awaits it pauses.
capitalize / lowerlize
Change the case of the first character and leave the rest as written. Use them to turn a model name such as
story into a class-style Story and back:apps/myapp/common/modelNames.ts
formatPhone / isPhoneNumber
formatPhone adds dashes to a Korean phone number as it is typed, and isPhoneNumber accepts only the dashed form. Field.Phone already runs both, so a form seldom calls them itself.| Call |
|---|
| ↳ Result |
| formatPhone("0101234567") |
| "010-123-4567" |
| formatPhone("010-123-45678") |
| "010-1234-5678" |
| formatPhone("01012345678") |
| "01012345678" |
| isPhoneNumber("010-1234-5678") |
| true |
| isPhoneNumber("031-123-4567") |
| true |
| isPhoneNumber("01012345678") |
| false |
| isPhoneNumber("02-1234-5678") |
| false |
- It counts characters, not area codes. At 10 characters it splits 3-3-4; at 13 it drops the dashes and splits 3-4-4. Any other length, 11 bare digits included, comes back unchanged.
- Why 10 and 13. One more digit typed after
010-123-4567makes 13 characters, which re-splits it as010-1234-5678. - Seoul's
02numbers do not fit.formatPhone("0212345678")gives021-234-5678, and the dashed02form failsisPhoneNumber.
In a form, bind
Field.Phone. It formats while the user types and shows an error for an invalid number:apps/myapp/lib/user/User.Template.tsx
isEmail
isEmail tells whether a string looks like an email address. It returns false for null, undefined and an empty string, so it needs no guard in front.| Call |
|---|
| ↳ Result |
| isEmail("user@example.com") |
| true |
| isEmail("user.name@example.co.kr") |
| true |
| isEmail("user+tag@example.com") |
| false |
| isEmail("user@example.c") |
| false |
| isEmail(null) |
| false |
+is not accepted. Before the@only letters, digits,_,.and-may appear, so plus-addressed mail fails.- The last domain part needs 2 to 8 characters, as in
.comor.co.kr. Input.Emailalready runs it and shows the invalid-email message. Call it yourself to gate a button or an action.
The shared sign-up form disables its button the same way:
apps/myapp/lib/org/Org.Util.tsx
RestClient
RestClient is a small fetch wrapper for a REST API that is not an Akan server. It keeps one base URL, shared headers and a timeout, and sends and parses JSON for you.Constructor options
Pass an options object, or just a base URL:
new RestClient("https://api.example.com") is short for { baseUrl: "https://api.example.com" }.baseUrlstring
Joined in front of a relative path, while an absolute
http(s) URL ignores it.headersHeadersInit
Sent with every request, and a call's own
headers win on a clash.timeoutnumber (ms)
Aborts a slower request; unset means no limit, and a call's own
timeout wins.Methods
MethodDescription
get<T>(url, options?)
Sends GET and resolves with the response body.
post<T>(url, data?, options?)
Sends POST with
data as the body.put<T>(url, data?, options?)
Sends PUT with
data as the body.delete<T>(url, options?)
Sends DELETE.
A client with shared headers and a timeout, plus a header on one call only:
apps/myapp/srvkit/exampleApi.ts
- Request body. A plain object is sent as JSON with
Content-Type: application/json. A string,FormData,URLSearchParams,BloborArrayBuffergoes as is. - Response. A JSON content type is parsed, and anything else resolves as text.
204and an empty body resolveundefined. - Failure. A non-2xx status rejects with a plain
Errorwhose message is the response body. In an adapter, catch it,logger.errorit and returnnull. - Four verbs only. There is no
patch.optionsalso takes otherfetchsettings such ascredentialsorcache.


Calling an Akan server? Use
fetch.*. RestClient knows nothing of signals, guards or Err, so a server Err arrives as a plain Error.pathGet / pathSet
Read or write a value deep inside an object by a path string. Reach for them when the path is data, such as a field name held in a variable.
SignatureDescription
pathGet(path, obj, separator = ".", fallback = null)
Returns the value at
path, or fallback when a step is missing or null.pathSet(obj, path, value)
Writes
value in place, creating missing objects and arrays, and returns the same obj.The same path in three spellings, and a write that builds what is missing:
apps/myapp/common/profilePath.ts
- The argument order differs.
pathGettakes the path first,pathSetthe object first. - Three spellings, one path.
links[0].url,links.0.urland["links", 0, "url"]reach the same value. Passing your ownseparatortopathGetturns the bracket form off. Mapfields work. AMapvalue is read and written throughgetandset, not as properties.


pathSet changes the object you pass. Copy it first if the original must stay. For store state, write a form path with st.do.writeOn<Model>(path, value) instead.randomPick / randomPicks
Pick random items from a list. Akan's test data generator
sampleOf fills an enum field with randomPick.SignatureDescription
randomPick(list)
One random item, or
undefined for an empty list.randomPicks(list, count = 1, allowDuplicate = false)
count random items, never the same one twice unless allowDuplicate is on.One pick, two distinct picks, and three picks that may repeat:
apps/myapp/lib/story.signal.spec.ts
- A short list comes back whole. With duplicates off and
countat or above the list length, you get the same array back, in order and not copied. - Not for secrets. They use
Math.random, so never build a token or a verification code with them.